Skip to main content

Overview

Once you’ve built a transaction, you need to sign it with the required keypairs and submit it to the Solana network. This guide covers transaction signing, using the RPC client to send transactions, and handling responses.

Signing Transactions

Signing with a Single Signer

import software.sava.core.tx.Transaction;
import software.sava.core.accounts.Signer;
import software.sava.core.encoding.Base58;

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

// Get a recent blockhash
byte[] recentBlockHash = // ... fetch from RPC (see below)

// Sign the transaction
transaction.sign(recentBlockHash, signer);

// Transaction is now ready to send
String base64Tx = transaction.base64EncodeToString();
Source: Transaction.java:671-678

Signing with Multiple Signers

import java.util.List;

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

// Sign with all required signers
transaction.sign(recentBlockHash, signers);

// Encode for submission
String base64Tx = transaction.base64EncodeToString();
Source: Transaction.java:698-706

Signing and Encoding in One Step

// Sign and encode to Base64 in one call
String base64SignedTx = transaction.signAndBase64Encode(
    recentBlockHash,
    signer
);

// With multiple signers
String base64SignedTx = transaction.signAndBase64Encode(
    recentBlockHash,
    signers
);
Source: Transaction.java:666-669, Transaction.java:693-696
1
Step 1: Get Recent Blockhash
2
Fetch a recent blockhash from the RPC endpoint.
3
Step 2: Sign Transaction
4
Call transaction.sign() with the blockhash and signer(s).
5
Step 3: Encode Transaction
6
Convert the signed transaction to Base64 format.
7
Step 4: Send to Network
8
Submit the encoded transaction via RPC.

Using the RPC Client to Send Transactions

Setting Recent Blockhash

import software.sava.rpc.json.http.client.SolanaRpcClient;
import software.sava.rpc.json.http.response.LatestBlockHash;
import java.net.http.HttpClient;
import java.net.URI;

// Create RPC client
HttpClient httpClient = HttpClient.newHttpClient();
URI endpoint = URI.create("https://api.mainnet-beta.solana.com");
SolanaRpcClient rpcClient = SolanaRpcClient.createClient(endpoint, httpClient);

// Get latest blockhash
LatestBlockHash latestBlockHash = rpcClient.getLatestBlockHash().join();
byte[] blockHash = latestBlockHash.blockHash();

// Set on transaction
transaction.setRecentBlockHash(blockHash);
Source: SolanaRpcClient.java:96-98

Sending with Sign Helper

The RPC client provides convenient methods to sign and send:
import java.util.concurrent.CompletableFuture;

// RPC client signs and sends the transaction
CompletableFuture<String> signatureFuture = rpcClient.sendTransaction(
    transaction,
    signer,
    blockHash
);

// Wait for signature
String signature = signatureFuture.join();
System.out.println("Transaction signature: " + signature);
Source: SolanaRpcClient.java:857-859

Sending with Multiple Signers

import java.util.List;
import java.util.SequencedCollection;

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

CompletableFuture<String> signatureFuture = rpcClient.sendTransaction(
    transaction,
    signers,
    blockHash
);

String signature = signatureFuture.join();
Source: SolanaRpcClient.java:866-873

Sending Pre-Signed Transactions

// Sign transaction yourself
String base64SignedTx = transaction.signAndBase64Encode(
    blockHash,
    signer
);

// Send pre-signed transaction
CompletableFuture<String> signatureFuture = rpcClient.sendTransaction(
    base64SignedTx
);

String signature = signatureFuture.join();
Source: SolanaRpcClient.java:875-877

Handling Transaction Responses

Basic Response Handling

CompletableFuture<String> future = rpcClient.sendTransaction(
    transaction,
    signer,
    blockHash
);

// Synchronous wait
try {
    String signature = future.join();
    System.out.println("Success! Signature: " + signature);
} catch (Exception e) {
    System.err.println("Transaction failed: " + e.getMessage());
}

Asynchronous Response Handling

rpcClient.sendTransaction(transaction, signer, blockHash)
    .thenAccept(signature -> {
        System.out.println("Transaction sent: " + signature);
    })
    .exceptionally(error -> {
        System.err.println("Failed to send: " + error.getMessage());
        return null;
    });

Getting Transaction Status

import software.sava.rpc.json.http.response.TxStatus;
import java.util.Map;
import java.util.List;

// Check signature status
List<String> signatures = List.of(signature);

CompletableFuture<Map<String, TxStatus>> statusFuture = 
    rpcClient.getSignatureStatuses(signatures);

Map<String, TxStatus> statuses = statusFuture.join();
TxStatus status = statuses.get(signature);

if (status != null) {
    System.out.println("Confirmations: " + status.confirmations());
    System.out.println("Slot: " + status.slot());
}
Source: SolanaRpcClient.java:746-751

Transaction Options

Using Preflight Commitment

import software.sava.rpc.json.http.request.Commitment;

// Specify preflight commitment level
CompletableFuture<String> signatureFuture = rpcClient.sendTransaction(
    Commitment.CONFIRMED, // preflight commitment
    transaction,
    signer,
    blockHash
);

String signature = signatureFuture.join();
Source: SolanaRpcClient.java:861-864

Skipping Preflight Checks

// Skip preflight for faster submission (less safe)
CompletableFuture<String> signatureFuture = 
    rpcClient.sendTransactionSkipPreflight(base64SignedTx);

String signature = signatureFuture.join();
Source: SolanaRpcClient.java:889-890

Setting Max Retries

int maxRetries = 3;

CompletableFuture<String> signatureFuture = rpcClient.sendTransaction(
    base64SignedTx,
    maxRetries
);

String signature = signatureFuture.join();
Source: SolanaRpcClient.java:879

Commitment Levels

Solana supports different commitment levels:
import software.sava.rpc.json.http.request.Commitment;

// Processed: Fastest, least certain
CompletableFuture<String> sig1 = rpcClient.sendTransaction(
    Commitment.PROCESSED,
    base64SignedTx,
    maxRetries
);

// Confirmed: Good balance of speed and certainty
CompletableFuture<String> sig2 = rpcClient.sendTransaction(
    Commitment.CONFIRMED,
    base64SignedTx,
    maxRetries
);

// Finalized: Slowest, most certain
CompletableFuture<String> sig3 = rpcClient.sendTransaction(
    Commitment.FINALIZED,
    base64SignedTx,
    maxRetries
);
Source: Commitment.java:7-11
CONFIRMED is the recommended default for most use cases, balancing speed and finality.

Real-World Example

Complete flow from building to confirming:
import software.sava.core.tx.Transaction;
import software.sava.core.tx.Instruction;
import software.sava.core.accounts.Signer;
import software.sava.rpc.json.http.client.SolanaRpcClient;
import software.sava.rpc.json.http.request.Commitment;
import java.net.http.HttpClient;
import java.util.List;

public class TransactionExample {
    public static void main(String[] args) {
        // Setup
        HttpClient httpClient = HttpClient.newHttpClient();
        var endpoint = URI.create("https://api.devnet.solana.com");
        var rpcClient = SolanaRpcClient.createClient(
            endpoint,
            httpClient,
            Commitment.CONFIRMED
        );
        
        Signer feePayer = // ... your signer
        List<Instruction> instructions = // ... your instructions
        
        // Build transaction
        Transaction transaction = Transaction.createTx(
            feePayer.publicKey(),
            instructions
        );
        
        // Get recent blockhash
        var latestBlockHash = rpcClient.getLatestBlockHash().join();
        byte[] blockHash = latestBlockHash.blockHash();
        
        // Send transaction (signs internally)
        var signatureFuture = rpcClient.sendTransaction(
            transaction,
            feePayer,
            blockHash
        );
        
        // Handle response
        signatureFuture
            .thenAccept(signature -> {
                System.out.println("Transaction sent: " + signature);
                
                // Monitor confirmation
                monitorTransaction(rpcClient, signature);
            })
            .exceptionally(error -> {
                System.err.println("Transaction failed: " + error.getMessage());
                return null;
            });
    }
    
    static void monitorTransaction(SolanaRpcClient rpcClient, String signature) {
        // Poll for confirmation
        var statusFuture = rpcClient.getSignatureStatuses(List.of(signature));
        
        statusFuture.thenAccept(statuses -> {
            var status = statuses.get(signature);
            if (status != null && status.confirmations() != null) {
                System.out.println("Confirmed in slot: " + status.slot());
            } else {
                System.out.println("Not yet confirmed");
            }
        });
    }
}

Error Handling

Handling RPC Errors

import software.sava.rpc.json.http.response.JsonRpcException;

try {
    String signature = rpcClient.sendTransaction(
        transaction,
        signer,
        blockHash
    ).join();
    System.out.println("Success: " + signature);
} catch (JsonRpcException e) {
    System.err.println("RPC Error: " + e.getMessage());
    // Handle specific error codes
} catch (Exception e) {
    System.err.println("General error: " + e.getMessage());
}

Transaction Simulation

Simulate before sending:
import software.sava.rpc.json.http.response.TxSimulation;

// Get fee for transaction
var feeFuture = rpcClient.getFeeForMessage(
    transaction.base64EncodeToString()
);

var fee = feeFuture.join();
System.out.println("Transaction fee: " + fee.value() + " lamports");
Source: SolanaRpcClient.java:92-94

Best Practices

  • Always use recent blockhashes (valid for ~60 seconds)
  • Fetch blockhash immediately before signing
  • Cache blockhash for multiple transactions within validity window
  • Always handle transaction failures gracefully
  • Implement retry logic for network issues
  • Log transaction signatures for tracking
  • Never log or expose private keys
  • Validate transaction contents before signing
  • Use appropriate commitment levels for your use case
  • Use async/await patterns for better throughput
  • Consider skip preflight for known-good transactions
  • Monitor transaction status asynchronously

Building Transactions

Learn how to construct transactions and instructions

Using RPC Client

Explore all RPC client features and methods

Build docs developers (and LLMs) love