Skip to main content

Overview

Transactions are the fundamental unit of state change on Solana. This guide covers creating instructions, building transactions, managing signers, and using the transaction skeleton pattern.

Creating Instructions

Instructions are the building blocks of transactions.

Basic Instruction Creation

import software.sava.core.tx.Instruction;
import software.sava.core.accounts.PublicKey;
import software.sava.core.accounts.meta.AccountMeta;
import java.util.List;

PublicKey programId = PublicKey.fromBase58Encoded("YourProgramId");

// Define instruction accounts
List<AccountMeta> accounts = List.of(
    AccountMeta.createWrite(sourceAccount),
    AccountMeta.createWrite(destinationAccount),
    AccountMeta.createReadOnlySigner(authority)
);

// Create instruction data
byte[] data = new byte[] { 0, 1, 2, 3 }; // Your instruction data

// Create the instruction
Instruction instruction = Instruction.createInstruction(
    programId,
    accounts,
    data
);
Source: Instruction.java:33-42

Using Program Account Meta

The program ID can be wrapped as invoked account metadata:
AccountMeta programMeta = AccountMeta.createInvoked(programId);

Instruction instruction = Instruction.createInstruction(
    programMeta,
    accounts,
    data
);
Source: Instruction.java:15-24

Adding Extra Accounts

Instructions support adding additional accounts:
Instruction baseInstruction = Instruction.createInstruction(
    programId,
    accounts,
    data
);

// Add single account
AccountMeta extraAccount = AccountMeta.createRead(somePublicKey);
Instruction withExtra = baseInstruction.extraAccount(extraAccount);

// Add multiple accounts
List<AccountMeta> extraAccounts = List.of(
    AccountMeta.createRead(account1),
    AccountMeta.createWrite(account2)
);
Instruction withMultiple = baseInstruction.extraAccounts(extraAccounts);
Source: Instruction.java:45-50

Building Transactions

Simple Transaction

Create a transaction from instructions:
import software.sava.core.tx.Transaction;
import software.sava.core.accounts.PublicKey;
import software.sava.core.tx.Instruction;
import java.util.List;

PublicKey feePayer = PublicKey.fromBase58Encoded("FeePayerAddress");

List<Instruction> instructions = List.of(
    instruction1,
    instruction2
);

// Create transaction
Transaction transaction = Transaction.createTx(feePayer, instructions);
Source: Transaction.java:90-91

Single Instruction Transaction

// For single instruction transactions
Transaction transaction = Transaction.createTx(feePayer, instruction);
Source: Transaction.java:177-179

Transaction Without Fee Payer

Create a transaction and add the fee payer later:
// Fee payer will be determined from signers
Transaction transaction = Transaction.createTx(instructions);
Source: Transaction.java:126-128
1
Step 1: Create Instructions
2
Define all instructions with their required accounts and data.
3
Step 2: Specify Fee Payer
4
Identify which account will pay transaction fees.
5
Step 3: Build Transaction
6
Call Transaction.createTx() with your instructions and fee payer.
7
Step 4: Set Recent Blockhash
8
Add a recent blockhash before signing (covered in the next guide).

Managing Signers

Multiple Signers

Transactions support multiple signers:
import software.sava.core.accounts.Signer;
import java.util.List;

Signer feePayer = // ... your fee payer signer
Signer authority1 = // ... first authority
Signer authority2 = // ... second authority

List<Signer> signers = List.of(
    feePayer,
    authority1,
    authority2
);

// Transaction will track required signers
Transaction transaction = Transaction.createTx(
    feePayer.publicKey(),
    instructions
);

Signer Account Metadata

Ensure signer accounts are marked correctly:
// Fee payer is special signer
AccountMeta feePayerMeta = AccountMeta.createFeePayer(feePayer.publicKey());

// Regular signer (read-only)
AccountMeta signerMeta = AccountMeta.createReadOnlySigner(signer.publicKey());

// Writable signer
AccountMeta writableSignerMeta = AccountMeta.createWritableSigner(
    authority.publicKey()
);
Source: AccountMeta.java:38-50

Using Address Lookup Tables

Lookup tables enable larger transactions by referencing accounts off-chain.

Versioned Transactions with Lookup Tables

import software.sava.core.accounts.lookup.AddressLookupTable;

AddressLookupTable lookupTable = // ... your lookup table

Transaction transaction = Transaction.createTx(
    feePayer,
    instructions,
    lookupTable
);

System.out.println("Transaction version: " + transaction.version());
Source: Transaction.java:130-139

Multiple Lookup Tables

import software.sava.core.accounts.meta.LookupTableAccountMeta;

LookupTableAccountMeta[] tables = // ... your lookup table metas

Transaction transaction = Transaction.createTx(
    feePayer,
    instructions,
    null, // no single lookup table
    tables // array of lookup tables
);
Source: Transaction.java:147-158

Transaction Skeleton Pattern

The skeleton pattern allows parsing and analyzing existing transactions.

Deserializing a Transaction

import software.sava.core.tx.TransactionSkeleton;

// From transaction data (e.g., fetched from RPC)
byte[] txData = // ... transaction bytes

TransactionSkeleton skeleton = TransactionSkeleton.deserializeSkeleton(txData);

// Parse instructions
Instruction[] instructions = skeleton.parseLegacyInstructions();

for (Instruction ix : instructions) {
    System.out.println("Program: " + ix.programId().publicKey().toBase58());
    System.out.println("Accounts: " + ix.accounts().size());
}
Source: SimpleTxSkeletonExample.java:20-21

Real-World Example

Parsing a Pump.fun transaction:
import software.sava.rpc.json.http.client.SolanaRpcClient;
import software.sava.core.tx.TransactionSkeleton;

// Fetch transaction from RPC
var tx = rpcClient.getTransaction(signature).join();

// Parse the skeleton
var skeleton = TransactionSkeleton.deserializeSkeleton(tx.data());
var instructions = skeleton.parseLegacyInstructions();

// Access specific instruction data
var buyIx = instructions[3];
var buyAccounts = buyIx.accounts();

PublicKey mint = buyAccounts.get(2).publicKey();
PublicKey buyer = buyAccounts.get(6).publicKey();

System.out.println("Mint: " + mint.toBase58());
System.out.println("Buyer: " + buyer.toBase58());
Source: SimpleTxSkeletonExample.java:15-37

Prepending and Appending Instructions

Modify existing transactions:
import software.sava.core.tx.Instruction;

Transaction originalTx = Transaction.createTx(feePayer, instructions);

// Prepend compute budget instruction
Instruction computeBudget = // ... create compute budget instruction
Transaction withBudget = originalTx.prependIx(computeBudget);

// Append additional instruction
Instruction extraIx = // ... additional instruction
Transaction extended = withBudget.appendIx(extraIx);

// Prepend multiple instructions
List<Instruction> priorityInstructions = List.of(
    computeBudgetIx,
    priorityFeeIx
);
Transaction withPriority = originalTx.prependInstructions(priorityInstructions);
Source: Transaction.java:746-754

Transaction Size Limits

Solana has transaction size limits:
Transaction transaction = Transaction.createTx(feePayer, instructions);

int size = transaction.size();
boolean exceedsLimit = transaction.exceedsSizeLimit();

if (exceedsLimit) {
    System.err.println("Transaction exceeds 1232 byte limit: " + size);
    // Consider using lookup tables or splitting into multiple transactions
}
Source: Transaction.java:21-26, Transaction.java:724-726
The maximum transaction size is 1232 bytes. Use address lookup tables for transactions that exceed this limit.

Advanced Patterns

Custom Account Sorting

Sava automatically sorts accounts for transactions:
import java.util.Map;
import java.util.HashMap;

// Accounts are automatically sorted: fee payer, signers, writable, read-only
Map<PublicKey, AccountMeta> accountMap = HashMap.newHashMap(64);

// Add accounts
for (Instruction ix : instructions) {
    for (AccountMeta meta : ix.accounts()) {
        accountMap.merge(meta.publicKey(), meta, Transaction.MERGE_ACCOUNT_META);
    }
}

// Sorted for legacy transactions
AccountMeta[] sorted = Transaction.sortLegacyAccounts(accountMap);

// Sorted for v0 transactions
AccountMeta[] sortedV0 = Transaction.sortV0Accounts(accountMap);
Source: Transaction.java:29-46, Transaction.java:84-88, Transaction.java:277-280

Replacing Instructions

Transaction transaction = Transaction.createTx(feePayer, instructions);

// Replace instruction at index
Instruction newInstruction = // ... replacement
Transaction modified = transaction.replaceInstruction(0, newInstruction);
Source: Transaction.java:756

Best Practices

  • Place compute budget instructions first
  • Group related instructions together
  • Minimize the number of instructions when possible
  • Mark accounts as writable only when necessary
  • Reuse account references within a transaction
  • Use lookup tables for frequently accessed accounts
  • Monitor transaction size with transaction.size()
  • Use exceedsSizeLimit() before signing
  • Consider versioned transactions for complex operations

Working with Accounts

Learn about keypairs and account metadata

Signing and Sending

Sign and submit your transactions to Solana

Build docs developers (and LLMs) love