Transactions are the fundamental unit of state change on Solana. The Sava SDK provides comprehensive support for creating, signing, and managing both legacy and versioned transactions.
Transaction Structure
A Solana transaction consists of:
- Signatures: Ed25519 signatures from required signers
- Message header: Number of signers and read-only accounts
- Account keys: All accounts referenced in the transaction
- Recent blockhash: Ensures transaction uniqueness and expiration
- Instructions: Program calls to execute
- Address lookup tables (v0 only): For account compression
Transaction Constants
import software.sava.core.tx.Transaction;
int MAX_SERIALIZED_LENGTH = Transaction.MAX_SERIALIZED_LENGTH; // 1232 bytes
int SIGNATURE_LENGTH = Transaction.SIGNATURE_LENGTH; // 64 bytes
int BLOCK_HASH_LENGTH = Transaction.BLOCK_HASH_LENGTH; // 32 bytes
int MAX_ACCOUNTS = Transaction.MAX_ACCOUNTS; // 64 accounts
Instructions
Instructions represent individual program calls within a transaction.
Creating Instructions
import software.sava.core.tx.Instruction;
import software.sava.core.accounts.meta.AccountMeta;
import java.util.List;
// Create instruction with program ID, accounts, and data
List<AccountMeta> accounts = List.of(
AccountMeta.createWrite(account1),
AccountMeta.createReadOnlySigner(signer),
AccountMeta.createRead(account2)
);
byte[] instructionData = ...; // Your instruction data
Instruction instruction = Instruction.createInstruction(
programId,
accounts,
instructionData
);
// With data offset and length
Instruction instruction = Instruction.createInstruction(
programId,
accounts,
dataBuffer,
offset,
length
);
// Using AccountMeta for program ID
Instruction instruction = Instruction.createInstruction(
AccountMeta.createInvoked(programId),
accounts,
instructionData
);
Working with Instructions
// Access instruction properties
AccountMeta program = instruction.programId();
List<AccountMeta> accounts = instruction.accounts();
byte[] data = instruction.data();
int dataLength = instruction.len();
int dataOffset = instruction.offset();
// Get serialized length
int length = instruction.serializedLength();
// Add extra accounts
Instruction updated = instruction.extraAccount(
AccountMeta.createRead(extraAccount)
);
Instruction updated = instruction.extraAccounts(
List.of(account1, account2)
);
// Copy instruction data
byte[] dataCopy = instruction.copyData();
Creating Transactions
Simple Transaction
import software.sava.core.tx.Transaction;
// Create transaction with single instruction
Transaction tx = Transaction.createTx(feePayer, instruction);
// Create with multiple instructions
Transaction tx = Transaction.createTx(
feePayer,
List.of(instruction1, instruction2, instruction3)
);
// Create without explicit fee payer (first signer is used)
Transaction tx = Transaction.createTx(List.of(instructions));
Versioned Transactions (v0)
Versioned transactions support address lookup tables for account compression:
import software.sava.core.accounts.lookup.AddressLookupTable;
// Create v0 transaction with lookup table
Transaction tx = Transaction.createTx(
feePayer,
instructions,
lookupTable
);
// With multiple lookup tables
LookupTableAccountMeta[] tables = ...; // Your lookup tables
Transaction tx = Transaction.createTx(
feePayer,
instructions,
tables
);
Use versioned transactions with lookup tables when you need to reference many accounts, as they reduce transaction size.
Transaction Skeleton
The TransactionSkeleton interface provides a read-only view of transaction structure without full deserialization.
Deserializing Skeletons
import software.sava.core.tx.TransactionSkeleton;
// Deserialize from signed transaction bytes
TransactionSkeleton skeleton = TransactionSkeleton.deserializeSkeleton(txBytes);
// Access transaction info
int version = skeleton.version();
boolean isVersioned = skeleton.isVersioned();
int numSignatures = skeleton.numSignatures();
String txId = skeleton.id();
byte[] blockHash = skeleton.blockHash();
String blockHashB58 = skeleton.base58BlockHash();
Inspecting Accounts
// Get account counts
int totalAccounts = skeleton.numAccounts();
int includedAccounts = skeleton.numIncludedAccounts();
int indexedAccounts = skeleton.numIndexedAccounts();
// Parse accounts
AccountMeta[] accounts = skeleton.parseAccounts();
// With lookup tables
AccountMeta[] accounts = skeleton.parseAccounts(lookupTableMap);
// Separate signers and non-signers
AccountMeta[] signers = skeleton.parseSignerAccounts();
PublicKey[] signerKeys = skeleton.parseSignerPublicKeys();
AccountMeta[] nonSigners = skeleton.parseNonSignerAccounts();
PublicKey[] programs = skeleton.parseProgramAccounts();
Parsing Instructions
// Parse all instructions
Instruction[] instructions = skeleton.parseInstructions(accounts);
// Parse legacy transaction instructions
Instruction[] instructions = skeleton.parseLegacyInstructions();
// Parse without full account data
Instruction[] instructions = skeleton.parseInstructionsWithoutAccounts();
// Parse without lookup table accounts
Instruction[] instructions = skeleton.parseInstructionsWithoutTableAccounts();
// Filter by discriminator
Instruction[] filtered = skeleton.filterInstructions(
accounts,
discriminator
);
Account Ordering
Solana requires accounts to be ordered in a specific way. The Sava SDK handles this automatically:
- Fee payer - Always first
- Writable signers - Accounts that sign and are modified
- Read-only signers - Accounts that sign but aren’t modified
- Writable accounts - Non-signer accounts that are modified
- Read-only accounts - Non-signer accounts that aren’t modified
// Legacy transaction ordering
AccountMeta[] sorted = Transaction.sortLegacyAccounts(mergedAccounts);
// V0 transaction ordering (includes invoked programs)
AccountMeta[] sorted = Transaction.sortV0Accounts(mergedAccounts);
The SDK automatically sorts and deduplicates accounts when creating transactions. If an account appears in multiple instructions with different permissions, the most permissive settings are used.
Setting Block Hash
Transactions require a recent blockhash to prevent replay attacks:
// Set blockhash on transaction
tx.setRecentBlockHash(blockHashBytes);
// Set from Base58 string
tx.setRecentBlockHash(blockHashString);
// Get current blockhash
byte[] currentHash = tx.recentBlockHash();
// Set on raw transaction data
Transaction.setBlockHash(txData, recentBlockHash);
Signing Transactions
Single Signer
import software.sava.core.accounts.Signer;
// Sign transaction
tx.sign(signer);
// Sign with blockhash
tx.sign(recentBlockHash, signer);
// Sign and encode to Base64
String signedTx = tx.signAndBase64Encode(signer);
String signedTx = tx.signAndBase64Encode(recentBlockHash, signer);
Multiple Signers
import java.util.List;
// Sign with multiple signers
List<Signer> signers = List.of(signer1, signer2, signer3);
tx.sign(signers);
// Sign with blockhash
tx.sign(recentBlockHash, signers);
// Sign and encode
String signedTx = tx.signAndBase64Encode(signers);
Static Signing Methods
// Sign raw transaction data
Transaction.sign(signer, txData);
// Sign with multiple signers
Transaction.sign(signers, txData);
// Sign and encode
String encoded = Transaction.signAndBase64Encode(signer, txData);
String encoded = Transaction.signAndBase64Encode(signers, txData);
Transaction Serialization
// Get serialized transaction
byte[] serialized = tx.serialized();
// Get size
int size = tx.size();
// Check if exceeds size limit
boolean tooLarge = tx.exceedsSizeLimit();
// Encode to Base64
String base64 = tx.base64EncodeToString();
Transaction ID
The transaction ID is the first signature:
// Get transaction ID
String txId = tx.getBase58Id();
byte[] txIdBytes = tx.getId();
// Get from raw signed transaction
String txId = Transaction.getBase58Id(signedTxBytes);
byte[] txIdBytes = Transaction.getId(signedTxBytes);
Modifying Transactions
// Prepend instruction
Transaction updated = tx.prependIx(newInstruction);
// Prepend multiple instructions
Transaction updated = tx.prependInstructions(ix1, ix2);
Transaction updated = tx.prependInstructions(instructionList);
// Append instruction
Transaction updated = tx.appendIx(newInstruction);
// Append multiple instructions
Transaction updated = tx.appendInstructions(instructionList);
// Replace instruction at index
Transaction updated = tx.replaceInstruction(index, newInstruction);
Modifying a transaction invalidates any existing signatures. You must re-sign after making changes.
Complete Example
import software.sava.core.tx.Transaction;
import software.sava.core.tx.Instruction;
import software.sava.core.accounts.meta.AccountMeta;
import software.sava.core.accounts.Signer;
import java.util.List;
// Create signers
Signer feePayer = ...; // Your fee payer signer
Signer authority = ...; // Your authority signer
// Build instruction
List<AccountMeta> accounts = List.of(
AccountMeta.createWrite(targetAccount),
AccountMeta.createWritableSigner(authority.publicKey()),
AccountMeta.createRead(systemProgram)
);
Instruction instruction = Instruction.createInstruction(
programId,
accounts,
instructionData
);
// Create transaction
Transaction tx = Transaction.createTx(
feePayer.publicKey(),
List.of(instruction)
);
// Set blockhash
tx.setRecentBlockHash(recentBlockHash);
// Sign with all required signers
tx.sign(List.of(feePayer, authority));
// Get Base64 encoded transaction for RPC
String signedTx = tx.base64EncodeToString();
// Get transaction ID
String txId = tx.getBase58Id();
Best Practices
- Check transaction size: Use
exceedsSizeLimit() before signing
- Order accounts properly: The SDK handles this automatically
- Use lookup tables: For transactions with many accounts
- Cache skeletons: When parsing many transactions, use skeletons for efficiency
- Validate blockhashes: Ensure blockhashes are recent (< 150 blocks old)
- Handle all signers: Collect all signatures before submitting
- Accounts - Understanding account metadata and signing
- Encoding - Serialization and Base58 encoding
- Cryptography - Ed25519 signing operations