
Solana Tx Building
- 196 installs
- 257 repo stars
- Updated June 24, 2026
- agiprolabs/claude-trading-skills
solana-tx-building is a Claude Code skill for constructing, simulating, and inspecting Solana transactions, including instruction building, compute budget, priority fees, and versioned transactions.
About
solana-tx-building is a Claude Code skill for constructing, simulating, and inspecting Solana transactions programmatically. It covers transaction anatomy, instruction and account-meta encoding, legacy vs versioned v0 transactions with Address Lookup Tables, compute budget and priority-fee instructions, and common patterns like SOL transfers. The skill's scripts never sign or submit real transactions, so a developer uses it for safe transaction construction and analysis.
- Transaction anatomy, instruction encoding, and account meta roles
- Legacy vs versioned (v0) transactions with Address Lookup Tables
- Compute budget, priority fees, and simulate-before-send safety
Solana Tx Building by the numbers
- 196 all-time installs (skills.sh)
- Ranked #101 of 479 Web3 & Blockchain skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
solana-tx-building capabilities & compatibility
- Capabilities
- transaction building · transaction simulation · priority fee estimation · compute budget
- Use cases
- api development · trading
- Runs
- Runs locally
- Pricing
- Free
What solana-tx-building says it does
This skill covers how to construct, simulate, and inspect Solana transactions programmatically.
The hard limit is **1232 bytes** for the entire serialized transaction.
npx skills add https://github.com/agiprolabs/claude-trading-skills --skill solana-tx-buildingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 196 |
|---|---|
| repo stars | ★ 257 |
| Last updated | June 24, 2026 |
| Repository | agiprolabs/claude-trading-skills ↗ |
What it does
Construct, simulate, and inspect Solana transactions including compute budget, priority fees, and v0 lookup tables.
Who is it for?
Assembling and analyzing Solana transactions with correct account metas, compute budget, and lookup tables.
Skip if: Signing or submitting live transactions, which the skill explicitly forbids in its scripts.
When should I use this skill?
You need to build or inspect a Solana transaction and understand v0/ALT, compute budget, or priority fees.
What you get
A correctly encoded Solana transaction, simulated and inspected, ready to hand off for signing elsewhere.
- Encoded and simulated Solana transactions
- Priority-fee and compute-budget instructions
By the numbers
- Transaction size limit 1232 bytes
- Legacy fits ~20-35 accounts
- Default 200,000 CU per instruction (max 1,400,000 per tx)
Files
Solana Transaction Building
This skill covers how to construct, simulate, and inspect Solana transactions programmatically. It addresses the full anatomy of a Solana transaction — from raw instruction encoding to versioned transaction formats, compute budget management, priority fees, and address lookup tables.
Safety: This skill is for transaction construction and analysis only. Scripts in this skill NEVER sign or submit real transactions. Always simulate before sending. Never auto-sign.
Transaction Anatomy
A Solana transaction consists of:
1. Signatures: One or more Ed25519 signatures (64 bytes each) 2. Message: The serializable payload containing:
- Header: Counts of required signers, read-only signers, read-only non-signers
- Account keys: Array of all pubkeys referenced by instructions
- Recent blockhash: 32-byte hash for replay protection (expires ~60-90 seconds)
- Instructions: Array of program calls
Transaction Size Limit
The hard limit is 1232 bytes for the entire serialized transaction. This constrains how many instructions and accounts you can include. Strategies to stay within the limit:
- Use versioned transactions with Address Lookup Tables (ALTs)
- Minimize the number of accounts per instruction
- Combine related operations into single instructions where supported
- Split complex operations across multiple transactions
Instruction Format
Each instruction contains three fields:
Instruction {
program_id_index: u8, // Index into the account keys array
accounts: [u8], // Indices into account keys array
data: [u8], // Opaque byte array interpreted by the program
}Account Meta
Every account referenced in an instruction has metadata:
AccountMeta {
pubkey: Pubkey, // 32-byte public key
is_signer: bool, // Must sign the transaction
is_writable: bool, // Will be written to by this instruction
}The four combinations determine the account's role:
| is_signer | is_writable | Role |
|---|---|---|
| true | true | Fee payer, token owner performing transfer |
| true | false | Multisig co-signer, read-only authority |
| false | true | Destination account, PDA being written |
| false | false | Program ID, sysvar, clock |
Legacy vs Versioned Transactions
Legacy Transactions
The original format. All accounts must be listed in the account keys array. With the 1232-byte limit, you can fit roughly 20-35 accounts depending on instruction data size.
Versioned Transactions (v0)
Introduced to support Address Lookup Tables (ALTs). A v0 transaction includes:
- A version prefix byte (
0x80for v0) - The same message structure as legacy
- An additional
address_table_lookupsarray
ALTs let you reference accounts by a compact index into an on-chain table rather than including the full 32-byte pubkey. This dramatically increases the number of accounts a transaction can reference.
AddressTableLookup {
account_key: Pubkey, // The ALT account address
writable_indexes: [u8], // Indices for writable accounts
readonly_indexes: [u8], // Indices for read-only accounts
}When to use v0: Any transaction referencing more than ~20 accounts, Jupiter swaps with multi-hop routes, complex DeFi interactions.
Compute Budget
Every transaction has a compute budget that determines how many compute units (CUs) it can consume and what priority fee to pay.
Compute Budget Instructions
Two key instructions from the Compute Budget Program (ComputeBudget111111111111111111111111111111):
1. Set Compute Unit Limit
Instruction data: [0x02, <units as u32 LE>]Sets the maximum CUs this transaction can consume. Default is 200,000 per instruction (max 1,400,000 per transaction). Setting this lower than needed causes the transaction to fail. Setting it higher wastes budget but does not cost more (you only pay for requested, not consumed).
2. Set Compute Unit Price
Instruction data: [0x03, <micro_lamports as u64 LE>]Sets the price per CU in micro-lamports. This is the priority fee mechanism. The total priority fee is:
priority_fee = compute_unit_limit * compute_unit_price / 1_000_000Priority Fee Estimation
To estimate an appropriate priority fee:
1. Call getRecentPrioritizationFees RPC method with the accounts your transaction touches 2. Look at the median or 75th percentile fee from recent slots 3. During congestion, fees spike — monitor and adjust dynamically
import httpx
def get_priority_fees(rpc_url: str, accounts: list[str]) -> list[dict]:
"""Fetch recent prioritization fees for given accounts."""
resp = httpx.post(rpc_url, json={
"jsonrpc": "2.0",
"id": 1,
"method": "getRecentPrioritizationFees",
"params": [accounts]
})
return resp.json()["result"]Common Transaction Patterns
1. SOL Transfer
The simplest transaction: a System Program transfer.
# System Program transfer instruction data layout:
# [2, 0, 0, 0] (u32 LE = instruction index 2 = Transfer)
# + amount as u64 LE (lamports)
import struct
def build_sol_transfer_data(lamports: int) -> bytes:
"""Build instruction data for a SOL transfer."""
return struct.pack("<I", 2) + struct.pack("<Q", lamports)Accounts required: 1. Sender (signer, writable) 2. Recipient (writable)
2. SPL Token Transfer
Transferring SPL tokens requires the Token Program.
# Token Program transfer instruction:
# [3] (instruction index 3 = Transfer)
# + amount as u64 LE
def build_token_transfer_data(amount: int) -> bytes:
"""Build instruction data for an SPL token transfer."""
return bytes([3]) + struct.pack("<Q", amount)Accounts required: 1. Source token account (writable) 2. Destination token account (writable) 3. Owner/delegate (signer)
3. Create Associated Token Account (ATA)
Before transferring tokens, the recipient must have an Associated Token Account.
# ATA Program: instruction index 0 = Create
# No instruction data needed (empty bytes)
ATA_PROGRAM_ID = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"Accounts required (in order): 1. Payer (signer, writable) — pays rent 2. Associated token account (writable) — the ATA to create 3. Wallet address — owner of the new ATA 4. Token mint 5. System Program 6. Token Program
4. Jupiter Swap
Jupiter provides a /swap-instructions endpoint that returns pre-built instructions. See the jupiter-api skill for full details.
General flow: 1. Get a quote from /quote 2. Get swap instructions from /swap-instructions 3. Build transaction with setup instructions + swap instruction + cleanup instructions 4. Add compute budget instructions 5. Simulate, then sign and send
Simulation
Always simulate before sending. Use the simulateTransaction RPC method:
def simulate_transaction(rpc_url: str, tx_base64: str) -> dict:
"""Simulate a transaction without submitting it.
Args:
rpc_url: Solana RPC endpoint URL.
tx_base64: Base64-encoded serialized transaction.
Returns:
Simulation result with logs and compute units consumed.
"""
resp = httpx.post(rpc_url, json={
"jsonrpc": "2.0",
"id": 1,
"method": "simulateTransaction",
"params": [
tx_base64,
{"encoding": "base64", "replaceRecentBlockhash": True}
]
})
result = resp.json()["result"]
if result["value"]["err"]:
print(f"Simulation failed: {result['value']['err']}")
for log in result["value"].get("logs", []):
print(f" {log}")
else:
cu = result["value"].get("unitsConsumed", 0)
print(f"Simulation OK — {cu} compute units consumed")
return resultThe replaceRecentBlockhash: True flag lets you simulate even if your blockhash has expired, which is useful for testing transaction construction without timing pressure.
Error Handling
Common transaction errors and their causes:
| Error | Cause | Fix |
|---|---|---|
BlockhashNotFound | Blockhash expired (~60-90s) | Fetch new blockhash and rebuild |
InsufficientFunds | Not enough SOL for fees + transfer | Check balance before building |
AccountNotFound | Token account doesn't exist | Create ATA first |
ProgramFailedToComplete | Exceeded compute budget | Increase compute unit limit |
TransactionTooLarge | Over 1232 bytes | Use ALTs or split into multiple txs |
InvalidAccountData | Wrong account passed to instruction | Verify account derivation |
SignatureVerificationFailed | Missing or wrong signer | Check all is_signer accounts signed |
Retry Strategy
import time
def send_with_retry(
rpc_url: str,
build_fn,
max_retries: int = 3,
base_delay: float = 0.5
) -> dict:
"""Build and send a transaction with blockhash refresh on expiry.
Args:
rpc_url: Solana RPC endpoint.
build_fn: Callable that takes a blockhash and returns a signed tx.
max_retries: Maximum retry attempts.
base_delay: Base delay between retries in seconds.
Returns:
Send result from RPC.
"""
for attempt in range(max_retries):
blockhash = get_latest_blockhash(rpc_url)
tx = build_fn(blockhash)
result = send_transaction(rpc_url, tx)
if "error" not in result:
return result
err = result["error"]
if "BlockhashNotFound" in str(err):
time.sleep(base_delay * (attempt + 1))
continue
raise RuntimeError(f"Transaction failed: {err}")
raise RuntimeError("Max retries exceeded")Transaction Decoding
To decode an existing transaction from the chain:
def decode_transaction(rpc_url: str, signature: str) -> dict:
"""Fetch and return a parsed transaction.
Args:
rpc_url: Solana RPC endpoint.
signature: Transaction signature (base58).
Returns:
Parsed transaction data.
"""
resp = httpx.post(rpc_url, json={
"jsonrpc": "2.0",
"id": 1,
"method": "getTransaction",
"params": [
signature,
{"encoding": "jsonParsed", "maxSupportedTransactionVersion": 0}
]
})
return resp.json()["result"]Integration with Other Skills
- `solana-rpc`: Provides the RPC connection layer for submitting and querying transactions
- `jupiter-api`: Supplies swap instructions that this skill assembles into transactions
- `dex-execution`: Orchestrates the full execution flow using transactions built by this skill
- `mev-analysis`: Evaluates MEV risk of constructed transactions before submission
- `helius-api`: Enhanced transaction parsing and webhook-based confirmation tracking
Safety Checklist
Before submitting any transaction to mainnet:
1. Simulate first — always call simulateTransaction before sendTransaction 2. Verify accounts — confirm all account addresses are correct (especially for token transfers) 3. Check balances — ensure sufficient SOL for fees and any transfers 4. Review compute budget — set appropriate CU limit based on simulation 5. Confirm priority fee — check current network fees, do not overpay 6. Never auto-sign — require explicit user confirmation before signing 7. Use devnet for testing — build and test on devnet before mainnet 8. Log everything — record transaction signatures, simulation results, and errors
Files
References
references/transaction_anatomy.md— Message format, versioned transactions, compute budget, blockhash managementreferences/common_instructions.md— Instruction layouts for System, Token, ATA, Compute Budget, Memo, and Jupiter programs
Scripts
scripts/build_transfer.py— Build and simulate a SOL transfer transaction (demo mode, never signs)scripts/decode_transaction.py— Fetch and decode on-chain transactions with program identification
Common Solana Instructions Reference
Program IDs
| Program | Address |
|---|---|
| System Program | 11111111111111111111111111111111 |
| Token Program | TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA |
| Token-2022 | TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb |
| Associated Token Account | ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL |
| Compute Budget | ComputeBudget111111111111111111111111111111 |
| Memo Program | MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr |
| Memo Program (v1) | Memo1UhkJBfCR6MNhJeXukzT2hbXzgnQ8e7MY1WDvN8 |
| Address Lookup Table | AddressLookupTab1e1111111111111111111111111 |
| Jupiter v6 | JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 |
System Program Instructions
Transfer SOL
- Instruction index: 2 (u32 LE)
- Data:
[02 00 00 00] + [amount as u64 LE]
import struct
def system_transfer_data(lamports: int) -> bytes:
"""Build System Program transfer instruction data."""
return struct.pack("<I", 2) + struct.pack("<Q", lamports)Accounts (in order): 1. Source (signer, writable) 2. Destination (writable)
Create Account
- Instruction index: 0 (u32 LE)
- Data:
[00 00 00 00] + [lamports u64 LE] + [space u64 LE] + [owner Pubkey 32 bytes]
def system_create_account_data(
lamports: int, space: int, owner: bytes
) -> bytes:
"""Build System Program createAccount instruction data."""
return (
struct.pack("<I", 0)
+ struct.pack("<Q", lamports)
+ struct.pack("<Q", space)
+ owner # 32 bytes
)Accounts: 1. Payer (signer, writable) 2. New account (signer, writable)
Allocate
- Instruction index: 8 (u32 LE)
- Data:
[08 00 00 00] + [space u64 LE]
Accounts: 1. Account to allocate (signer, writable)
Token Program Instructions
Transfer
- Instruction index: 3
- Data:
[03] + [amount u64 LE]
def token_transfer_data(amount: int) -> bytes:
"""Build Token Program transfer instruction data."""
return bytes([3]) + struct.pack("<Q", amount)Accounts: 1. Source token account (writable) 2. Destination token account (writable) 3. Owner (signer)
Transfer Checked
- Instruction index: 12
- Data:
[0c] + [amount u64 LE] + [decimals u8]
def token_transfer_checked_data(amount: int, decimals: int) -> bytes:
"""Build Token Program transferChecked instruction data."""
return bytes([12]) + struct.pack("<Q", amount) + bytes([decimals])Accounts: 1. Source token account (writable) 2. Token mint (read-only) 3. Destination token account (writable) 4. Owner (signer)
Preferred over plain transfer because it validates the mint and decimals.
Approve
- Instruction index: 4
- Data:
[04] + [amount u64 LE]
Accounts: 1. Source token account (writable) 2. Delegate (read-only) 3. Owner (signer)
Mint To
- Instruction index: 7
- Data:
[07] + [amount u64 LE]
Accounts: 1. Mint (writable) 2. Destination token account (writable) 3. Mint authority (signer)
Burn
- Instruction index: 8
- Data:
[08] + [amount u64 LE]
Accounts: 1. Source token account (writable) 2. Mint (writable) 3. Owner (signer)
Associated Token Account Program
Create ATA
- Instruction index: 0 (implicit — no instruction data)
- Data: empty (
b"")
def ata_create_data() -> bytes:
"""ATA create instruction has no data."""
return b""Accounts (in order): 1. Payer (signer, writable) — pays rent for new account 2. Associated token account (writable) — the ATA to create 3. Wallet address (read-only) — owner of the ATA 4. Token mint (read-only) 5. System Program (read-only) 6. Token Program (read-only)
Create Idempotent
- Instruction index: 1
- Data:
[01]
Same accounts as Create ATA. Does not error if the ATA already exists. Preferred for transaction builders since you don't need to check existence first.
ATA Derivation
The ATA address is a PDA derived from:
seeds = [wallet_address, token_program_id, mint_address]
program_id = ATA_PROGRAM_IDimport hashlib
def derive_ata(wallet: bytes, mint: bytes) -> bytes:
"""Derive Associated Token Account address (simplified).
Note: This is illustrative. Production code should use
a proper PDA derivation with bump seed search.
"""
TOKEN_PROGRAM = bytes.fromhex(
"06ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a9"
)
ATA_PROGRAM = bytes.fromhex(
"8c97258f4e2489f1bb3d1029148e0d830b5a1399daff1084048e7bd8dbe9f859"
)
# findProgramAddress searches for bump 255..0
for bump in range(255, -1, -1):
seed = wallet + TOKEN_PROGRAM + mint + bytes([bump])
candidate = hashlib.sha256(seed + ATA_PROGRAM + b"ProgramDerivedAddress").digest()
# Valid PDA must not be on the ed25519 curve (simplified check omitted)
return candidate
raise ValueError("Could not derive ATA")Compute Budget Program
Set Compute Unit Limit
- Discriminator:
0x02 - Data:
[02] + [units u32 LE]
def compute_budget_set_units(units: int) -> bytes:
"""Set compute unit limit for the transaction."""
return bytes([2]) + struct.pack("<I", units)Accounts: None required.
Set Compute Unit Price
- Discriminator:
0x03 - Data:
[03] + [micro_lamports u64 LE]
def compute_budget_set_price(micro_lamports: int) -> bytes:
"""Set compute unit price (priority fee) in micro-lamports."""
return bytes([3]) + struct.pack("<Q", micro_lamports)Accounts: None required.
Request Heap Frame
- Discriminator:
0x01 - Data:
[01] + [bytes u32 LE]
Must be a multiple of 1024. Maximum 262,144 (256 KB).
Accounts: None required.
Memo Program
Add Memo
- Data: UTF-8 encoded memo string (arbitrary bytes)
def memo_data(message: str) -> bytes:
"""Build memo instruction data."""
return message.encode("utf-8")Accounts: At least one signer (optional, but recommended for attribution).
The memo is stored in the transaction log and is visible in explorers. Maximum length is constrained by transaction size.
Jupiter Swap Instructions
Jupiter provides swap instructions via the /swap-instructions API endpoint rather than requiring manual construction.
API Flow
import httpx
def get_jupiter_swap_instructions(
quote: dict,
user_pubkey: str,
) -> dict:
"""Get swap instructions from Jupiter API.
Args:
quote: Quote response from /quote endpoint.
user_pubkey: The user's wallet public key.
Returns:
Dict with setupInstructions, swapInstruction,
cleanupInstruction, and addressLookupTableAddresses.
"""
resp = httpx.post(
"https://quote-api.jup.ag/v6/swap-instructions",
json={
"quoteResponse": quote,
"userPublicKey": user_pubkey,
"dynamicComputeUnitLimit": True,
"prioritizationFeeLamports": "auto",
},
)
resp.raise_for_status()
return resp.json()The response contains pre-encoded instructions that you deserialize and include in your transaction. See the jupiter-api skill for full endpoint documentation.
Solana Transaction Anatomy
Message Format
A Solana transaction message contains four sections:
1. Header (3 bytes)
MessageHeader {
num_required_signatures: u8, // Total signers required
num_readonly_signed_accounts: u8, // Signers that are read-only
num_readonly_unsigned_accounts: u8, // Non-signers that are read-only
}The header determines how accounts in the account keys array are categorized:
Account keys array layout:
[0..num_required_signatures) → writable signers
[num_required_signatures - num_readonly_signed..num_required_signatures) → read-only signers
[num_required_signatures..len - num_readonly_unsigned) → writable non-signers
[len - num_readonly_unsigned..len) → read-only non-signersThe first account in the writable signers section is always the fee payer.
2. Account Keys
A compact array of 32-byte public keys. Every account referenced by any instruction must appear here exactly once. The order matters because instructions reference accounts by index.
Deduplication rules:
- If an account is both writable and read-only across instructions, it is listed as writable
- If an account is both a signer and non-signer across instructions, it is listed as a signer
- The most permissive role wins
3. Recent Blockhash
A 32-byte hash that:
- Prevents replay attacks (same transaction cannot be submitted twice)
- Expires after ~60-90 seconds (~150 slots)
- Must be fetched fresh before building each transaction
4. Instructions
A compact array of compiled instructions:
CompiledInstruction {
program_id_index: u8, // Index of the program in account keys
accounts: [u8], // Indices of accounts in account keys
data: [u8], // Program-specific instruction data
}Versioned Transactions
Legacy Format
[signatures_length, ...signatures, message_bytes]Where message_bytes is:
[header(3), account_keys_length, ...account_keys(32 each),
recent_blockhash(32), instructions_length, ...compiled_instructions]V0 Format
V0 transactions add a version prefix and address table lookups:
[0x80, // version prefix (0x80 = v0)
signatures_length, ...signatures,
message_bytes,
address_table_lookups_length, ...address_table_lookups]Each address table lookup:
AddressTableLookup {
account_key: Pubkey, // 32 bytes — the ALT account
writable_indexes: [u8], // Compact array of writable indices
readonly_indexes: [u8], // Compact array of read-only indices
}Address Lookup Tables (ALTs)
ALTs are on-chain accounts that store up to 256 public keys. Instead of including a full 32-byte pubkey in the transaction, you reference it with a 1-byte index into the ALT.
Space savings: Each ALT reference saves 31 bytes per account (32-byte pubkey replaced by 1-byte index, plus the 32-byte ALT address amortized across lookups).
Creating an ALT:
# AddressLookupTable program: AddressLookupTab1e1111111111111111111111111
# Instruction 0: CreateLookupTable
# Instruction 1: ExtendLookupTable (add addresses)
# Instruction 2: FreezeLookupTable
# Instruction 3: DeactivateLookupTable
# Instruction 4: CloseLookupTableLookup flow: 1. Transaction includes ALT account key + indices 2. Runtime loads the ALT account data 3. Resolves each index to the stored pubkey 4. Passes resolved accounts to the program
Restrictions:
- ALT accounts must be active (not deactivated)
- Newly added addresses require one slot to become usable
- Maximum 256 addresses per ALT
- Multiple ALTs can be used in a single transaction
Compute Budget
Default Limits
| Parameter | Default | Maximum |
|---|---|---|
| Compute units per instruction | 200,000 | — |
| Compute units per transaction | 200,000 * num_instructions | 1,400,000 |
| Heap size | 32 KB | 256 KB |
| Call depth | — | 4 |
| Stack frame size | — | 4 KB |
Compute Budget Instructions
Program ID: ComputeBudget111111111111111111111111111111
Set Compute Unit Limit (instruction discriminator: 0x02):
Data layout: [0x02, units: u32 LE]
Example: Set 300,000 CUs → [0x02, 0xe0, 0x93, 0x04, 0x00]Set Compute Unit Price (instruction discriminator: 0x03):
Data layout: [0x03, micro_lamports: u64 LE]
Example: Set 1000 micro-lamports → [0x03, 0xe8, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]Request Heap Frame (instruction discriminator: 0x01):
Data layout: [0x01, bytes: u32 LE]
Must be multiple of 1024, max 256 KB (262144)Priority Fee Calculation
priority_fee_lamports = compute_unit_limit * compute_unit_price_micro_lamports / 1_000_000Example: 300,000 CU limit at 5,000 micro-lamports/CU:
300,000 * 5,000 / 1,000,000 = 1,500 lamports = 0.0000015 SOLThe base transaction fee (5,000 lamports per signature) is always charged in addition to the priority fee.
Transaction Size: 1232 Bytes
The maximum transaction size is 1232 bytes. This is a hard network limit (MTU-derived). Here is how the bytes break down for a typical transaction:
| Component | Size |
|---|---|
| Signatures length (compact-u16) | 1 byte |
| Each signature | 64 bytes |
| Header | 3 bytes |
| Account keys length (compact-u16) | 1-2 bytes |
| Each account key | 32 bytes |
| Recent blockhash | 32 bytes |
| Instructions length (compact-u16) | 1-2 bytes |
| Each instruction (variable) | ~5-100+ bytes |
Budget estimation for a single-signer transaction:
Fixed overhead: 64 (sig) + 1 (sig count) + 3 (header) + 32 (blockhash)
+ 2 (account count) + 2 (instruction count) = 104 bytes
Remaining for accounts + instructions: 1232 - 104 = 1128 bytes
Each account key: 32 bytes → ~35 accounts max with minimal instruction dataStrategies to Fit Within 1232 Bytes
1. Use v0 transactions with ALTs — compress account references 2. Minimize accounts — only include necessary accounts 3. Batch wisely — split large operations across transactions 4. Use compact instruction data — avoid unnecessary padding
Blockhash Management
Fetching a Blockhash
import httpx
def get_latest_blockhash(rpc_url: str) -> dict:
"""Get latest blockhash and its last valid block height.
Returns:
Dict with 'blockhash' (str) and 'lastValidBlockHeight' (int).
"""
resp = httpx.post(rpc_url, json={
"jsonrpc": "2.0", "id": 1,
"method": "getLatestBlockhash",
"params": [{"commitment": "finalized"}]
})
return resp.json()["result"]["value"]Blockhash Expiry and Retry
- A blockhash is valid for ~150 slots (~60-90 seconds)
lastValidBlockHeighttells you the exact block height after which the blockhash expires- If a transaction fails with
BlockhashNotFound, fetch a new blockhash and rebuild - For time-sensitive operations, use
commitment: "confirmed"for faster (but slightly less safe) blockhashes - For high-value operations, use
commitment: "finalized"and accept the latency
Durable Nonces (Advanced)
For transactions that need to remain valid longer than ~90 seconds:
1. Create a nonce account with SystemProgram.createNonceAccount 2. Use the nonce value as the "blockhash" in your transaction 3. Include an AdvanceNonce instruction as the first instruction 4. The transaction remains valid until the nonce is advanced
Use cases: offline signing, multi-party signatures, scheduled transactions.
#!/usr/bin/env python3
"""Build and inspect a SOL transfer transaction (simulation only).
This script demonstrates Solana transaction construction from scratch:
- Building System Program transfer instructions
- Adding compute budget instructions (unit limit + priority fee)
- Calculating transaction size
- Simulating via RPC (if available)
- Printing a full transaction breakdown
SAFETY: This script NEVER signs or submits real transactions.
It operates in demo/simulation mode only.
Usage:
python scripts/build_transfer.py
python scripts/build_transfer.py --sender <PUBKEY> --recipient <PUBKEY> --amount 0.01
python scripts/build_transfer.py --demo
Dependencies:
uv pip install httpx
Environment Variables:
SOLANA_RPC_URL: Solana RPC endpoint (optional, uses devnet by default)
"""
import argparse
import base64
import hashlib
import json
import os
import struct
import sys
from typing import Optional
# ── Configuration ───────────────────────────────────────────────────
DEFAULT_RPC_URL = "https://api.devnet.solana.com"
SOLANA_RPC_URL = os.getenv("SOLANA_RPC_URL", DEFAULT_RPC_URL)
# Well-known program IDs
SYSTEM_PROGRAM_ID = "11111111111111111111111111111111"
COMPUTE_BUDGET_PROGRAM_ID = "ComputeBudget111111111111111111111111111111"
# Demo keypairs (NOT real keys — for structure demonstration only)
DEMO_SENDER = "11111111111111111111111111111112"
DEMO_RECIPIENT = "11111111111111111111111111111113"
LAMPORTS_PER_SOL = 1_000_000_000
# ── Base58 Encoding ─────────────────────────────────────────────────
ALPHABET = b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
def base58_decode(s: str) -> bytes:
"""Decode a base58-encoded string to bytes.
Args:
s: Base58-encoded string.
Returns:
Decoded bytes.
"""
n = 0
for char in s:
n = n * 58 + ALPHABET.index(char.encode())
result = n.to_bytes(max(1, (n.bit_length() + 7) // 8), "big")
# Handle leading '1's (zero bytes in base58)
pad = 0
for char in s:
if char == "1":
pad += 1
else:
break
return b"\x00" * pad + result
def base58_encode(data: bytes) -> str:
"""Encode bytes to a base58 string.
Args:
data: Raw bytes to encode.
Returns:
Base58-encoded string.
"""
n = int.from_bytes(data, "big")
result = ""
while n > 0:
n, remainder = divmod(n, 58)
result = ALPHABET[remainder:remainder + 1].decode() + result
# Handle leading zero bytes
for byte in data:
if byte == 0:
result = "1" + result
else:
break
return result or "1"
# ── Compact Array Encoding ──────────────────────────────────────────
def encode_compact_u16(value: int) -> bytes:
"""Encode an integer as a Solana compact-u16.
Args:
value: Integer to encode (0-65535).
Returns:
Encoded bytes (1-3 bytes).
"""
if value < 0x80:
return bytes([value])
elif value < 0x4000:
return bytes([
(value & 0x7F) | 0x80,
(value >> 7) & 0x7F,
])
else:
return bytes([
(value & 0x7F) | 0x80,
((value >> 7) & 0x7F) | 0x80,
(value >> 14) & 0x03,
])
# ── Instruction Builders ────────────────────────────────────────────
def build_system_transfer_data(lamports: int) -> bytes:
"""Build instruction data for a System Program transfer.
Args:
lamports: Amount to transfer in lamports.
Returns:
Encoded instruction data bytes.
"""
# Instruction index 2 = Transfer, then amount as u64 LE
return struct.pack("<I", 2) + struct.pack("<Q", lamports)
def build_compute_unit_limit_data(units: int) -> bytes:
"""Build instruction data to set compute unit limit.
Args:
units: Maximum compute units for the transaction.
Returns:
Encoded instruction data bytes.
"""
return bytes([0x02]) + struct.pack("<I", units)
def build_compute_unit_price_data(micro_lamports: int) -> bytes:
"""Build instruction data to set compute unit price (priority fee).
Args:
micro_lamports: Price per compute unit in micro-lamports.
Returns:
Encoded instruction data bytes.
"""
return bytes([0x03]) + struct.pack("<Q", micro_lamports)
# ── Transaction Message Builder ──────────────────────────────────────
class TransactionBuilder:
"""Builds a Solana transaction message (legacy format).
This builder collects instructions and their account references,
deduplicates accounts, sorts them by role, and serializes the
complete message.
Attributes:
fee_payer: The pubkey (bytes) of the fee payer.
instructions: List of (program_id_bytes, accounts_list, data_bytes).
"""
def __init__(self, fee_payer: bytes) -> None:
"""Initialize the builder with a fee payer.
Args:
fee_payer: 32-byte public key of the fee payer.
"""
self.fee_payer: bytes = fee_payer
self.instructions: list[tuple[bytes, list[tuple[bytes, bool, bool]], bytes]] = []
def add_instruction(
self,
program_id: bytes,
accounts: list[tuple[bytes, bool, bool]],
data: bytes,
) -> "TransactionBuilder":
"""Add an instruction to the transaction.
Args:
program_id: 32-byte program public key.
accounts: List of (pubkey, is_signer, is_writable) tuples.
data: Instruction data bytes.
Returns:
Self for chaining.
"""
self.instructions.append((program_id, accounts, data))
return self
def _collect_accounts(self) -> list[tuple[bytes, bool, bool]]:
"""Collect and deduplicate all accounts across instructions.
Returns:
Sorted list of (pubkey, is_signer, is_writable) with deduplication.
"""
account_map: dict[bytes, tuple[bool, bool]] = {}
# Fee payer is always signer + writable
account_map[self.fee_payer] = (True, True)
for program_id, accounts, _ in self.instructions:
# Program ID is read-only, non-signer
if program_id not in account_map:
account_map[program_id] = (False, False)
for pubkey, is_signer, is_writable in accounts:
if pubkey in account_map:
existing_signer, existing_writable = account_map[pubkey]
account_map[pubkey] = (
existing_signer or is_signer,
existing_writable or is_writable,
)
else:
account_map[pubkey] = (is_signer, is_writable)
# Sort: writable signers first, then readonly signers,
# then writable non-signers, then readonly non-signers
# Fee payer must be first
result: list[tuple[bytes, bool, bool]] = []
for pubkey, (is_signer, is_writable) in account_map.items():
result.append((pubkey, is_signer, is_writable))
def sort_key(item: tuple[bytes, bool, bool]) -> tuple[int, int, bytes]:
pubkey, is_signer, is_writable = item
if pubkey == self.fee_payer:
return (0, 0, pubkey)
if is_signer and is_writable:
return (1, 0, pubkey)
if is_signer and not is_writable:
return (1, 1, pubkey)
if not is_signer and is_writable:
return (2, 0, pubkey)
return (2, 1, pubkey)
result.sort(key=sort_key)
return result
def build_message(self, recent_blockhash: bytes) -> bytes:
"""Serialize the transaction message.
Args:
recent_blockhash: 32-byte recent blockhash.
Returns:
Serialized message bytes.
"""
accounts = self._collect_accounts()
pubkey_to_index = {acc[0]: i for i, acc in enumerate(accounts)}
# Count header values
num_signers = sum(1 for _, s, _ in accounts if s)
num_readonly_signed = sum(1 for _, s, w in accounts if s and not w)
num_readonly_unsigned = sum(1 for _, s, w in accounts if not s and not w)
# Header
header = bytes([num_signers, num_readonly_signed, num_readonly_unsigned])
# Account keys
account_keys = b"".join(acc[0] for acc in accounts)
# Compile instructions
compiled_instructions = b""
for program_id, inst_accounts, data in self.instructions:
prog_index = pubkey_to_index[program_id]
acc_indices = bytes([pubkey_to_index[a[0]] for a in inst_accounts])
compiled_instructions += (
bytes([prog_index])
+ encode_compact_u16(len(acc_indices))
+ acc_indices
+ encode_compact_u16(len(data))
+ data
)
# Assemble message
message = (
header
+ encode_compact_u16(len(accounts))
+ account_keys
+ recent_blockhash
+ encode_compact_u16(len(self.instructions))
+ compiled_instructions
)
return message
def get_info(self, recent_blockhash: bytes) -> dict:
"""Get transaction information without signing.
Args:
recent_blockhash: 32-byte recent blockhash.
Returns:
Dict with transaction details: size, accounts, instructions.
"""
accounts = self._collect_accounts()
message = self.build_message(recent_blockhash)
num_signers = sum(1 for _, s, _ in accounts if s)
# Transaction size = compact(num_signatures) + signatures + message
sig_size = 1 + (num_signers * 64) # compact-u16(1) + 64 bytes per sig
total_size = sig_size + len(message)
return {
"message_size": len(message),
"total_size_with_signatures": total_size,
"num_accounts": len(accounts),
"num_signers": num_signers,
"num_instructions": len(self.instructions),
"size_limit": 1232,
"bytes_remaining": 1232 - total_size,
"accounts": [
{
"index": i,
"pubkey": base58_encode(acc[0]),
"is_signer": acc[1],
"is_writable": acc[2],
}
for i, acc in enumerate(accounts)
],
}
# ── RPC Helpers ──────────────────────────────────────────────────────
def get_latest_blockhash(rpc_url: str) -> Optional[dict]:
"""Fetch the latest blockhash from an RPC endpoint.
Args:
rpc_url: Solana RPC URL.
Returns:
Dict with 'blockhash' and 'lastValidBlockHeight', or None on error.
"""
try:
import httpx
resp = httpx.post(
rpc_url,
json={
"jsonrpc": "2.0",
"id": 1,
"method": "getLatestBlockhash",
"params": [{"commitment": "finalized"}],
},
timeout=10.0,
)
resp.raise_for_status()
data = resp.json()
if "error" in data:
print(f"RPC error: {data['error']}")
return None
return data["result"]["value"]
except ImportError:
print("httpx not installed. Install with: uv pip install httpx")
return None
except Exception as e:
print(f"Failed to fetch blockhash: {e}")
return None
def simulate_transaction(rpc_url: str, message_base64: str) -> Optional[dict]:
"""Simulate a transaction via RPC.
Args:
rpc_url: Solana RPC URL.
message_base64: Base64-encoded transaction message.
Returns:
Simulation result dict, or None on error.
"""
try:
import httpx
resp = httpx.post(
rpc_url,
json={
"jsonrpc": "2.0",
"id": 1,
"method": "simulateTransaction",
"params": [
message_base64,
{
"encoding": "base64",
"sigVerify": False,
"replaceRecentBlockhash": True,
},
],
},
timeout=15.0,
)
resp.raise_for_status()
data = resp.json()
if "error" in data:
print(f"RPC error: {data['error']}")
return None
return data["result"]["value"]
except ImportError:
print("httpx not installed. Install with: uv pip install httpx")
return None
except Exception as e:
print(f"Simulation request failed: {e}")
return None
# ── Demo Mode ────────────────────────────────────────────────────────
def run_demo() -> None:
"""Run a demonstration build of a SOL transfer transaction.
Builds the transaction structure, prints the breakdown,
and optionally simulates if an RPC endpoint is available.
NEVER signs or submits the transaction.
"""
print("=" * 60)
print("SOLANA TRANSACTION BUILDER — DEMO MODE")
print("=" * 60)
print()
print("WARNING: This is a demonstration only.")
print("No transactions will be signed or submitted.")
print()
# Use demo pubkeys (32 zero-padded bytes)
sender = base58_decode(DEMO_SENDER)
recipient = base58_decode(DEMO_RECIPIENT)
amount_sol = 0.01
amount_lamports = int(amount_sol * LAMPORTS_PER_SOL)
print(f"Sender: {DEMO_SENDER}")
print(f"Recipient: {DEMO_RECIPIENT}")
print(f"Amount: {amount_sol} SOL ({amount_lamports} lamports)")
print()
# Build compute budget program ID
compute_budget_id = base58_decode(COMPUTE_BUDGET_PROGRAM_ID)
system_program_id = base58_decode(SYSTEM_PROGRAM_ID)
# Build transaction
builder = TransactionBuilder(fee_payer=sender)
# Instruction 1: Set compute unit limit
cu_limit = 200_000
builder.add_instruction(
program_id=compute_budget_id,
accounts=[],
data=build_compute_unit_limit_data(cu_limit),
)
# Instruction 2: Set priority fee
priority_fee_micro_lamports = 1_000 # 1000 micro-lamports per CU
builder.add_instruction(
program_id=compute_budget_id,
accounts=[],
data=build_compute_unit_price_data(priority_fee_micro_lamports),
)
# Instruction 3: SOL transfer
builder.add_instruction(
program_id=system_program_id,
accounts=[
(sender, True, True), # Source: signer, writable
(recipient, False, True), # Destination: writable
],
data=build_system_transfer_data(amount_lamports),
)
# Use a dummy blockhash for demo
dummy_blockhash = b"\x00" * 32
# Get transaction info
info = builder.get_info(dummy_blockhash)
print("─" * 60)
print("TRANSACTION BREAKDOWN")
print("─" * 60)
print(f"Instructions: {info['num_instructions']}")
print(f"Accounts: {info['num_accounts']}")
print(f"Signers: {info['num_signers']}")
print(f"Message size: {info['message_size']} bytes")
print(f"Total size: {info['total_size_with_signatures']} bytes")
print(f"Size limit: {info['size_limit']} bytes")
print(f"Bytes remaining: {info['bytes_remaining']} bytes")
print()
print("ACCOUNTS:")
for acc in info["accounts"]:
role_parts = []
if acc["is_signer"]:
role_parts.append("signer")
if acc["is_writable"]:
role_parts.append("writable")
role = ", ".join(role_parts) if role_parts else "read-only"
pubkey_short = acc["pubkey"][:16] + "..."
print(f" [{acc['index']}] {pubkey_short} ({role})")
print()
print("INSTRUCTIONS:")
instruction_names = [
"SetComputeUnitLimit (200,000 CUs)",
f"SetComputeUnitPrice ({priority_fee_micro_lamports} micro-lamports/CU)",
f"SystemProgram.Transfer ({amount_sol} SOL)",
]
for i, name in enumerate(instruction_names):
print(f" [{i}] {name}")
print()
# Calculate priority fee
priority_fee = cu_limit * priority_fee_micro_lamports / 1_000_000
base_fee = 5_000 # lamports per signature
total_fee = base_fee + priority_fee
print("FEE BREAKDOWN:")
print(f" Base fee: {base_fee} lamports ({base_fee / LAMPORTS_PER_SOL:.9f} SOL)")
print(f" Priority fee: {priority_fee:.0f} lamports ({priority_fee / LAMPORTS_PER_SOL:.9f} SOL)")
print(f" Total fee: {total_fee:.0f} lamports ({total_fee / LAMPORTS_PER_SOL:.9f} SOL)")
print()
# Build the message for serialization demo
message = builder.build_message(dummy_blockhash)
# Create a mock transaction (with zero signatures for demo)
num_signers = info["num_signers"]
mock_tx = (
encode_compact_u16(num_signers)
+ (b"\x00" * 64 * num_signers) # zero signatures (unsigned)
+ message
)
tx_base64 = base64.b64encode(mock_tx).decode()
print("SERIALIZED (base64, unsigned — for inspection only):")
print(f" {tx_base64[:80]}...")
print(f" Length: {len(mock_tx)} bytes")
print()
# Attempt RPC simulation if available
print("─" * 60)
print("SIMULATION")
print("─" * 60)
if SOLANA_RPC_URL == DEFAULT_RPC_URL:
print(f"Using default devnet RPC: {DEFAULT_RPC_URL}")
print("Set SOLANA_RPC_URL for a custom endpoint.")
else:
print(f"Using RPC: {SOLANA_RPC_URL}")
print()
print("Attempting simulation (this will likely fail with demo addresses)...")
sim_result = simulate_transaction(SOLANA_RPC_URL, tx_base64)
if sim_result is not None:
if sim_result.get("err"):
print(f"Simulation error (expected with demo data): {sim_result['err']}")
if sim_result.get("logs"):
print("Logs:")
for log_line in sim_result["logs"][:10]:
print(f" {log_line}")
else:
cu_used = sim_result.get("unitsConsumed", 0)
print(f"Simulation succeeded — {cu_used} compute units consumed")
else:
print("Simulation unavailable (no RPC connection or httpx not installed)")
print()
print("=" * 60)
print("DEMO COMPLETE — No transaction was signed or submitted.")
print("=" * 60)
def run_custom(sender: str, recipient: str, amount_sol: float) -> None:
"""Build a custom SOL transfer transaction (still never signs/sends).
Args:
sender: Base58-encoded sender public key.
recipient: Base58-encoded recipient public key.
amount_sol: Amount to transfer in SOL.
"""
print("=" * 60)
print("SOLANA TRANSACTION BUILDER — CUSTOM BUILD")
print("=" * 60)
print()
print("WARNING: This builds a transaction structure for inspection.")
print("No transaction will be signed or submitted.")
print()
sender_bytes = base58_decode(sender)
recipient_bytes = base58_decode(recipient)
amount_lamports = int(amount_sol * LAMPORTS_PER_SOL)
compute_budget_id = base58_decode(COMPUTE_BUDGET_PROGRAM_ID)
system_program_id = base58_decode(SYSTEM_PROGRAM_ID)
builder = TransactionBuilder(fee_payer=sender_bytes)
# Compute budget instructions
cu_limit = 200_000
cu_price = 1_000
builder.add_instruction(
program_id=compute_budget_id,
accounts=[],
data=build_compute_unit_limit_data(cu_limit),
)
builder.add_instruction(
program_id=compute_budget_id,
accounts=[],
data=build_compute_unit_price_data(cu_price),
)
# Transfer instruction
builder.add_instruction(
program_id=system_program_id,
accounts=[
(sender_bytes, True, True),
(recipient_bytes, False, True),
],
data=build_system_transfer_data(amount_lamports),
)
# Try to get real blockhash
print("Fetching latest blockhash...")
bh_result = get_latest_blockhash(SOLANA_RPC_URL)
if bh_result:
blockhash = base58_decode(bh_result["blockhash"])
print(f"Blockhash: {bh_result['blockhash']}")
print(f"Last valid block height: {bh_result['lastValidBlockHeight']}")
else:
blockhash = b"\x00" * 32
print("Using dummy blockhash (RPC unavailable)")
print()
info = builder.get_info(blockhash)
print(f"Sender: {sender}")
print(f"Recipient: {recipient}")
print(f"Amount: {amount_sol} SOL ({amount_lamports} lamports)")
print()
print(f"Total transaction size: {info['total_size_with_signatures']} / {info['size_limit']} bytes")
print(f"Accounts: {info['num_accounts']} | Signers: {info['num_signers']} | Instructions: {info['num_instructions']}")
print()
for acc in info["accounts"]:
role = []
if acc["is_signer"]:
role.append("signer")
if acc["is_writable"]:
role.append("writable")
print(f" Account [{acc['index']}]: {acc['pubkey'][:20]}... ({', '.join(role) or 'read-only'})")
print()
print("Transaction built successfully (NOT signed, NOT submitted).")
print("=" * 60)
# ── Main ─────────────────────────────────────────────────────────────
def main() -> None:
"""Parse arguments and run the appropriate mode."""
parser = argparse.ArgumentParser(
description="Build a SOL transfer transaction for inspection (never signs/sends)"
)
parser.add_argument(
"--demo",
action="store_true",
default=True,
help="Run in demo mode with placeholder addresses (default)",
)
parser.add_argument("--sender", type=str, help="Sender public key (base58)")
parser.add_argument("--recipient", type=str, help="Recipient public key (base58)")
parser.add_argument(
"--amount",
type=float,
default=0.01,
help="Amount in SOL (default: 0.01)",
)
args = parser.parse_args()
if args.sender and args.recipient:
run_custom(args.sender, args.recipient, args.amount)
else:
run_demo()
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Fetch and decode a Solana transaction from the chain.
Retrieves a transaction by its signature, then decodes and displays:
- All instructions with program identification
- Account keys and their roles (signer, writable)
- Compute units consumed and fees paid
- Inner instructions (CPI calls)
In --demo mode, displays a hardcoded transaction structure without
requiring an RPC connection.
Usage:
python scripts/decode_transaction.py --demo
python scripts/decode_transaction.py --signature <TX_SIGNATURE>
python scripts/decode_transaction.py --signature <TX_SIGNATURE> --rpc <RPC_URL>
Dependencies:
uv pip install httpx
Environment Variables:
SOLANA_RPC_URL: Solana RPC endpoint (optional)
HELIUS_API_KEY: Helius API key for enhanced RPC (optional)
"""
import argparse
import json
import os
import sys
from typing import Any, Optional
# ── Configuration ───────────────────────────────────────────────────
DEFAULT_RPC_URL = "https://api.mainnet-beta.solana.com"
SOLANA_RPC_URL = os.getenv("SOLANA_RPC_URL", "")
HELIUS_API_KEY = os.getenv("HELIUS_API_KEY", "")
# ── Known Programs ──────────────────────────────────────────────────
KNOWN_PROGRAMS: dict[str, str] = {
"11111111111111111111111111111111": "System Program",
"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA": "Token Program",
"TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb": "Token-2022 Program",
"ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL": "Associated Token Account Program",
"ComputeBudget111111111111111111111111111111": "Compute Budget Program",
"MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr": "Memo Program v2",
"Memo1UhkJBfCR6MNhJeXukzT2hbXzgnQ8e7MY1WDvN8": "Memo Program v1",
"JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4": "Jupiter v6",
"JUP4Fb2cqiRUcaTHdrPC8h2gNsA2ETXiPDD33WcGuJB": "Jupiter v4",
"JUP2jxvXaqu7NQY1GmNF4m1vodw12LVXYxbFL2uXvfo": "Jupiter v2",
"675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8": "Raydium AMM v4",
"CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK": "Raydium CLMM",
"whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc": "Orca Whirlpool",
"9W959DqEETiGZocYWCQPaJ6sBmUzgfxXfqGeTEdp3aQP": "Orca Swap v2",
"SSwpkEEcbUqx4vtoEByFjSkhKdCT862DNVb52nZg1UZ": "Saber Stable Swap",
"LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo": "Meteora DLMM",
"Eo7WjKq67rjJQSZxS6z3YkapzY3eMj6Xy8X5EQVn5UaB": "Meteora Pools",
"6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P": "Pump.fun",
"TSWAPaqyCSx2KABk68Shruf4rp7CxcNi8hAsbdwmHbN": "Tensor Swap",
"M2mx93ekt1fmXSVkTrUL9xVFHkmME8HTUi5Cyc5aF7K": "Magic Eden v2",
"Auth4MyMPJWFAtBnJFrSdbNWWQ3QrHnQrjPnWPgqurp": "Magic Eden Auth",
"Vote111111111111111111111111111111111111111": "Vote Program",
"Stake11111111111111111111111111111111111111": "Stake Program",
"AddressLookupTab1e1111111111111111111111111": "Address Lookup Table Program",
"metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s": "Metaplex Token Metadata",
"So1endDq2YkqhipRh3WViPa8hFvz0XP1SOERDCz3EHQ": "Solend",
"JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4": "Jupiter v6",
"jitotL4nLKSn6RzRJUYS1PGGYkjTCUJAUEcgBNcFimj": "Jito Tip Program",
}
def identify_program(program_id: str) -> str:
"""Identify a program by its address.
Args:
program_id: Base58-encoded program public key.
Returns:
Human-readable program name, or "Unknown Program" with truncated address.
"""
if program_id in KNOWN_PROGRAMS:
return KNOWN_PROGRAMS[program_id]
return f"Unknown ({program_id[:16]}...)"
# ── RPC Functions ────────────────────────────────────────────────────
def get_rpc_url(override: Optional[str] = None) -> str:
"""Determine the RPC URL to use.
Priority: CLI arg > SOLANA_RPC_URL env > Helius > default.
Args:
override: Optional CLI-provided RPC URL.
Returns:
The RPC URL to use.
"""
if override:
return override
if SOLANA_RPC_URL:
return SOLANA_RPC_URL
if HELIUS_API_KEY:
return f"https://mainnet.helius-rpc.com/?api-key={HELIUS_API_KEY}"
return DEFAULT_RPC_URL
def fetch_transaction(rpc_url: str, signature: str) -> Optional[dict]:
"""Fetch a transaction from the Solana RPC.
Args:
rpc_url: Solana RPC endpoint URL.
signature: Transaction signature (base58).
Returns:
Parsed transaction data, or None on error.
"""
try:
import httpx
except ImportError:
print("Error: httpx is required. Install with: uv pip install httpx")
return None
try:
resp = httpx.post(
rpc_url,
json={
"jsonrpc": "2.0",
"id": 1,
"method": "getTransaction",
"params": [
signature,
{
"encoding": "jsonParsed",
"maxSupportedTransactionVersion": 0,
},
],
},
timeout=15.0,
)
resp.raise_for_status()
data = resp.json()
if "error" in data:
print(f"RPC error: {data['error']}")
return None
if data.get("result") is None:
print(f"Transaction not found: {signature}")
print("The transaction may not exist or the RPC may not have historical data.")
return None
return data["result"]
except Exception as e:
print(f"Failed to fetch transaction: {e}")
return None
# ── Transaction Decoder ─────────────────────────────────────────────
def decode_and_display(tx_data: dict, signature: str = "N/A") -> None:
"""Decode and display a transaction in human-readable format.
Args:
tx_data: Parsed transaction data from getTransaction RPC call.
signature: Transaction signature for display.
"""
print("=" * 70)
print("TRANSACTION DECODER")
print("=" * 70)
print()
# Basic info
print(f"Signature: {signature}")
slot = tx_data.get("slot", "N/A")
print(f"Slot: {slot}")
block_time = tx_data.get("blockTime")
if block_time:
from datetime import datetime, timezone
dt = datetime.fromtimestamp(block_time, tz=timezone.utc)
print(f"Time: {dt.isoformat()}")
print()
# Transaction status
meta = tx_data.get("meta", {})
err = meta.get("err")
if err:
print(f"STATUS: FAILED — {err}")
else:
print("STATUS: SUCCESS")
print()
# Version
version = tx_data.get("version", "legacy")
print(f"Version: {version}")
print()
# Fees
fee = meta.get("fee", 0)
print("─" * 70)
print("FEES")
print("─" * 70)
print(f"Transaction fee: {fee} lamports ({fee / 1_000_000_000:.9f} SOL)")
compute_consumed = meta.get("computeUnitsConsumed")
if compute_consumed is not None:
print(f"Compute units: {compute_consumed:,}")
if fee > 5000 and compute_consumed > 0:
priority_fee = fee - 5000 # subtract base fee
micro_lamports_per_cu = (priority_fee * 1_000_000) / compute_consumed
print(f"Priority fee: ~{priority_fee} lamports ({micro_lamports_per_cu:.0f} micro-lamports/CU)")
print()
# Account keys
transaction = tx_data.get("transaction", {})
message = transaction.get("message", {})
account_keys = message.get("accountKeys", [])
print("─" * 70)
print(f"ACCOUNTS ({len(account_keys)})")
print("─" * 70)
for i, acc in enumerate(account_keys):
if isinstance(acc, dict):
pubkey = acc.get("pubkey", "?")
is_signer = acc.get("signer", False)
is_writable = acc.get("writable", False)
source = acc.get("source", "transaction")
else:
pubkey = acc
is_signer = False
is_writable = False
source = "transaction"
roles = []
if is_signer:
roles.append("signer")
if is_writable:
roles.append("writable")
if source == "lookupTable":
roles.append("ALT")
role_str = f" ({', '.join(roles)})" if roles else ""
# Check if this is a known program
program_name = KNOWN_PROGRAMS.get(pubkey, "")
label = f" ← {program_name}" if program_name else ""
print(f" [{i:2d}] {pubkey}{role_str}{label}")
print()
# Instructions
instructions = message.get("instructions", [])
print("─" * 70)
print(f"INSTRUCTIONS ({len(instructions)})")
print("─" * 70)
for i, ix in enumerate(instructions):
_display_instruction(ix, i, account_keys)
print()
# Inner instructions (CPI calls)
inner_instructions = meta.get("innerInstructions", [])
if inner_instructions:
print("─" * 70)
print("INNER INSTRUCTIONS (CPI calls)")
print("─" * 70)
for inner_group in inner_instructions:
ix_index = inner_group.get("index", "?")
inner_ixs = inner_group.get("instructions", [])
print(f"\n Triggered by instruction [{ix_index}]:")
for j, inner_ix in enumerate(inner_ixs):
_display_instruction(inner_ix, j, account_keys, indent=" ")
print()
# Balance changes
pre_balances = meta.get("preBalances", [])
post_balances = meta.get("postBalances", [])
if pre_balances and post_balances:
print("─" * 70)
print("SOL BALANCE CHANGES")
print("─" * 70)
for i, (pre, post) in enumerate(zip(pre_balances, post_balances)):
diff = post - pre
if diff != 0:
pubkey = _get_pubkey(account_keys, i)
direction = "+" if diff > 0 else ""
print(f" {pubkey[:24]}... {direction}{diff / 1_000_000_000:.9f} SOL")
print()
# Token balance changes
pre_token = meta.get("preTokenBalances", [])
post_token = meta.get("postTokenBalances", [])
if pre_token or post_token:
print("─" * 70)
print("TOKEN BALANCE CHANGES")
print("─" * 70)
_display_token_changes(pre_token, post_token, account_keys)
print()
# Log messages (truncated)
logs = meta.get("logMessages", [])
if logs:
print("─" * 70)
print(f"LOG MESSAGES ({len(logs)} lines, showing first 20)")
print("─" * 70)
for log_line in logs[:20]:
print(f" {log_line}")
if len(logs) > 20:
print(f" ... and {len(logs) - 20} more lines")
print()
print("=" * 70)
def _display_instruction(
ix: dict,
index: int,
account_keys: list,
indent: str = " ",
) -> None:
"""Display a single instruction.
Args:
ix: Instruction data dict.
index: Instruction index for display.
account_keys: Full account keys list for resolving indices.
indent: Indentation prefix.
"""
# jsonParsed format has "parsed" field for known programs
if "parsed" in ix:
parsed = ix["parsed"]
program = ix.get("program", "unknown")
program_id = ix.get("programId", "?")
program_name = identify_program(program_id)
if isinstance(parsed, dict):
ix_type = parsed.get("type", "?")
info = parsed.get("info", {})
print(f"{indent}[{index}] {program_name}: {ix_type}")
# Display key info fields
for key, value in info.items():
if isinstance(value, (str, int, float)):
print(f"{indent} {key}: {value}")
elif isinstance(value, dict) and len(value) <= 3:
for k, v in value.items():
print(f"{indent} {key}.{k}: {v}")
else:
print(f"{indent}[{index}] {program_name}: {parsed}")
return
# Raw format
program_id_index = ix.get("programIdIndex")
if program_id_index is not None:
program_id = _get_pubkey(account_keys, program_id_index)
else:
program_id = ix.get("programId", "?")
program_name = identify_program(program_id)
print(f"{indent}[{index}] {program_name}")
# Account indices
acc_indices = ix.get("accounts", [])
if acc_indices and len(acc_indices) <= 8:
acc_strs = []
for ai in acc_indices:
if isinstance(ai, int):
pk = _get_pubkey(account_keys, ai)
acc_strs.append(f"{pk[:12]}...")
else:
acc_strs.append(str(ai)[:12] + "...")
print(f"{indent} accounts: [{', '.join(acc_strs)}]")
elif acc_indices:
print(f"{indent} accounts: [{len(acc_indices)} accounts]")
# Data
data = ix.get("data", "")
if data and len(data) <= 40:
print(f"{indent} data: {data}")
elif data:
print(f"{indent} data: {data[:40]}... ({len(data)} chars)")
def _get_pubkey(account_keys: list, index: int) -> str:
"""Get a pubkey string from account keys by index.
Args:
account_keys: List of account keys (str or dict).
index: Index into the list.
Returns:
Public key string, or "?" if index is out of range.
"""
if index >= len(account_keys):
return "?"
acc = account_keys[index]
if isinstance(acc, dict):
return acc.get("pubkey", "?")
return str(acc)
def _display_token_changes(
pre: list[dict],
post: list[dict],
account_keys: list,
) -> None:
"""Display token balance changes between pre and post states.
Args:
pre: Pre-transaction token balances.
post: Post-transaction token balances.
account_keys: Full account keys list.
"""
# Build lookup of pre-balances by account index
pre_map: dict[int, dict] = {}
for entry in pre:
idx = entry.get("accountIndex", -1)
pre_map[idx] = entry
post_map: dict[int, dict] = {}
for entry in post:
idx = entry.get("accountIndex", -1)
post_map[idx] = entry
all_indices = set(pre_map.keys()) | set(post_map.keys())
for idx in sorted(all_indices):
pre_entry = pre_map.get(idx, {})
post_entry = post_map.get(idx, {})
mint = (
post_entry.get("mint")
or pre_entry.get("mint")
or "?"
)
owner = (
post_entry.get("owner")
or pre_entry.get("owner")
or "?"
)
pre_amount_str = (
pre_entry.get("uiTokenAmount", {}).get("uiAmountString", "0")
)
post_amount_str = (
post_entry.get("uiTokenAmount", {}).get("uiAmountString", "0")
)
try:
pre_amount = float(pre_amount_str) if pre_amount_str else 0.0
post_amount = float(post_amount_str) if post_amount_str else 0.0
except ValueError:
pre_amount = 0.0
post_amount = 0.0
diff = post_amount - pre_amount
if abs(diff) > 0:
direction = "+" if diff > 0 else ""
print(f" Owner: {owner[:20]}...")
print(f" Mint: {mint[:20]}...")
print(f" Change: {direction}{diff}")
print()
# ── Demo Mode ────────────────────────────────────────────────────────
DEMO_TRANSACTION: dict[str, Any] = {
"slot": 250000000,
"blockTime": 1709251200,
"version": 0,
"transaction": {
"message": {
"accountKeys": [
{"pubkey": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU", "signer": True, "writable": True, "source": "transaction"},
{"pubkey": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", "signer": False, "writable": True, "source": "transaction"},
{"pubkey": "So11111111111111111111111111111111111111112", "signer": False, "writable": True, "source": "transaction"},
{"pubkey": "ComputeBudget111111111111111111111111111111", "signer": False, "writable": False, "source": "transaction"},
{"pubkey": "JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4", "signer": False, "writable": False, "source": "transaction"},
{"pubkey": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", "signer": False, "writable": False, "source": "transaction"},
{"pubkey": "11111111111111111111111111111111", "signer": False, "writable": False, "source": "transaction"},
{"pubkey": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL", "signer": False, "writable": False, "source": "transaction"},
{"pubkey": "whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc", "signer": False, "writable": False, "source": "transaction"},
{"pubkey": "D8cy77BBepLMngZx6ZukaTff5hCt1HrWyKk3Hnd9oitf", "signer": False, "writable": True, "source": "lookupTable"},
{"pubkey": "3NxDBWt55PEoCWCiH5t2KpGNzEb8Bhi5XAYyxEPCFm7T", "signer": False, "writable": True, "source": "lookupTable"},
],
"instructions": [
{
"programId": "ComputeBudget111111111111111111111111111111",
"parsed": {"type": "setComputeUnitLimit", "info": {"units": 300000}},
"program": "computeBudget",
},
{
"programId": "ComputeBudget111111111111111111111111111111",
"parsed": {"type": "setComputeUnitPrice", "info": {"microLamports": 5000}},
"program": "computeBudget",
},
{
"programId": "JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4",
"parsed": {"type": "route", "info": {"inputMint": "So11111111111111111111111111111111111111112", "outputMint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", "inAmount": "1000000000", "slippageBps": 50}},
"program": "jupiter",
},
],
},
},
"meta": {
"err": None,
"fee": 6500,
"computeUnitsConsumed": 185432,
"preBalances": [2500000000, 0, 1000000000, 1, 1, 1, 1, 1, 1, 500000000, 300000000],
"postBalances": [1499993500, 0, 0, 1, 1, 1, 1, 1, 1, 500000000, 300000000],
"preTokenBalances": [
{"accountIndex": 1, "mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", "owner": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU", "uiTokenAmount": {"uiAmountString": "0", "decimals": 6}},
],
"postTokenBalances": [
{"accountIndex": 1, "mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", "owner": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU", "uiTokenAmount": {"uiAmountString": "150.5", "decimals": 6}},
],
"innerInstructions": [
{
"index": 2,
"instructions": [
{
"programId": "whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc",
"parsed": {"type": "swap", "info": {"amountIn": "1000000000", "amountOut": "150500000"}},
"program": "whirlpool",
},
{
"programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
"parsed": {"type": "transfer", "info": {"amount": "1000000000", "source": "So11...", "destination": "D8cy..."}},
"program": "spl-token",
},
{
"programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
"parsed": {"type": "transfer", "info": {"amount": "150500000", "source": "3Nxd...", "destination": "EPjF..."}},
"program": "spl-token",
},
],
}
],
"logMessages": [
"Program ComputeBudget111111111111111111111111111111 invoke [1]",
"Program ComputeBudget111111111111111111111111111111 success",
"Program ComputeBudget111111111111111111111111111111 invoke [1]",
"Program ComputeBudget111111111111111111111111111111 success",
"Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 invoke [1]",
"Program log: Instruction: Route",
"Program whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc invoke [2]",
"Program log: Instruction: Swap",
"Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [3]",
"Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success",
"Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [3]",
"Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success",
"Program whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc success",
"Program JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4 success",
],
},
}
def run_demo() -> None:
"""Run demo mode with a hardcoded transaction structure."""
print("Running in DEMO mode with a simulated Jupiter swap transaction.")
print("(SOL → USDC via Orca Whirlpool, routed through Jupiter v6)")
print()
decode_and_display(
DEMO_TRANSACTION,
signature="5demo...ExampleSignatureNotReal...xyz",
)
def run_live(signature: str, rpc_override: Optional[str] = None) -> None:
"""Fetch and decode a live transaction from the chain.
Args:
signature: Transaction signature to look up.
rpc_override: Optional RPC URL override.
"""
rpc_url = get_rpc_url(rpc_override)
print(f"Fetching transaction from: {rpc_url[:50]}...")
print(f"Signature: {signature}")
print()
tx_data = fetch_transaction(rpc_url, signature)
if tx_data is None:
print("Could not fetch transaction. Check the signature and RPC endpoint.")
sys.exit(1)
decode_and_display(tx_data, signature=signature)
# ── Main ─────────────────────────────────────────────────────────────
def main() -> None:
"""Parse arguments and run the decoder."""
parser = argparse.ArgumentParser(
description="Decode and display a Solana transaction"
)
parser.add_argument(
"--demo",
action="store_true",
help="Run with a hardcoded demo transaction",
)
parser.add_argument(
"--signature", "-s",
type=str,
help="Transaction signature to decode",
)
parser.add_argument(
"--rpc",
type=str,
help="RPC URL override",
)
args = parser.parse_args()
if args.demo:
run_demo()
elif args.signature:
run_live(args.signature, args.rpc)
else:
print("No signature provided. Running in demo mode.")
print("Use --signature <TX_SIG> to decode a real transaction.")
print()
run_demo()
if __name__ == "__main__":
main()
Related skills
FAQ
What is the Solana transaction size limit?
The hard limit is 1232 bytes for the entire serialized transaction, which is why versioned transactions with Address Lookup Tables are used to reference more accounts.
Do the skill's scripts send transactions?
No. The skill is for construction and analysis only; its scripts never sign or submit real transactions and always simulate before sending.