Skip to main content
The Sava SDK provides comprehensive encoding and serialization utilities for working with Solana data formats. This includes Base58 encoding for addresses, Borsh serialization for on-chain data, and compact integer encoding for efficient transaction packing.

Base58 Encoding

Base58 is used throughout Solana for human-readable representation of addresses, transaction IDs, and other binary data.

Encoding to Base58

import software.sava.core.encoding.Base58;

// Encode byte array to Base58 string
byte[] data = new byte[32];
String encoded = Base58.encode(data);

// Encode portion of array
String encoded = Base58.encode(data, offset, length);

// Encode to char array
char[] output = new char[44];  // 32 bytes -> ~44 chars
int startPos = Base58.encode(data, output);
String result = new String(output, startPos, output.length - startPos);

Decoding from Base58

// Decode Base58 string to bytes
byte[] decoded = Base58.decode("11111111111111111111111111111111");

// Decode char array
char[] encoded = "abc123".toCharArray();
byte[] decoded = Base58.decode(encoded);

// Decode portion of char array
byte[] decoded = Base58.decode(encoded, from, length);

Validation

// Check if character is valid Base58
boolean valid = Base58.isBase58('a');  // true
boolean valid = Base58.isBase58('0');  // true (zero is valid)
boolean valid = Base58.isBase58('O');  // false (capital O excluded)

// Check if string is valid Base58
boolean valid = Base58.isBase58("11111111111111111111111111111111");

// Find first non-Base58 character
int invalidIndex = Base58.nonBase58("abc!def");  // returns 3

Alphabet and Exclusions

Base58 uses a subset of alphanumeric characters, excluding similar-looking ones:
// Excluded characters: 0 (zero), O (capital o), I (capital i), l (lowercase L)
// Alphabet: 123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz
Base58 excludes 0, O, I, and l to avoid confusion when reading addresses. This is why Solana addresses always start with numbers 1-9 or letters (excluding O, I, l).

Mutable Encoding (Advanced)

For performance-critical code, use mutable encoding to avoid allocations:
// Mutable encode (modifies input buffer)
char[] output = new char[44];
int outputStart = Base58.mutableEncode(inputBytes, output);

// Begin mutable encode (for large data)
long state = Base58.beginMutableEncode(input, maxLength, output);

// Continue mutable encode
int outputStart = Base58.continueMutableEncode(
    input,
    leadingZeros,
    inputStart,
    outputStart,
    output
);

Borsh Serialization

Borsh (Binary Object Representation Serializer for Hashing) is Solana’s standard for on-chain data serialization.

Primitive Types

Booleans

import software.sava.core.borsh.Borsh;

// Write boolean
byte[] data = new byte[100];
int offset = 0;
offset += Borsh.write(true, data, offset);

// Write optional boolean
offset += Borsh.writeOptional(Boolean.TRUE, data, offset);
offset += Borsh.writeOptional(null, data, offset);  // writes 0

// Read boolean array
boolean[] values = new boolean[5];
Borsh.readArray(values, data, offset);

// Calculate length
int length = Borsh.lenOptional(Boolean.TRUE);  // 2 bytes

Integers

// Write integers (little-endian)
offset += Borsh.writeArray(new byte[]{1, 2, 3}, data, offset);
offset += Borsh.writeArray(new short[]{100, 200}, data, offset);
offset += Borsh.writeArray(new int[]{1000, 2000}, data, offset);
offset += Borsh.writeArray(new long[]{100000L, 200000L}, data, offset);

// Write optional integers
offset += Borsh.writeOptional(OptionalInt.of(42), data, offset);
offset += Borsh.writeOptional(OptionalLong.of(42L), data, offset);

// Read integers
int[] values = Borsh.readintVector(data, offset);
long[] longs = Borsh.readlongVector(data, offset);

Floats and Doubles

// Write floating point (little-endian)
offset += Borsh.writeArray(new float[]{1.5f, 2.5f}, data, offset);
offset += Borsh.writeArray(new double[]{1.5, 2.5}, data, offset);

// Write optional
offset += Borsh.writeOptional(OptionalDouble.of(3.14), data, offset);

// Read floating point
float[] floats = Borsh.readfloatVector(data, offset);
double[] doubles = Borsh.readdoubleVector(data, offset);

128-bit Integers

import java.math.BigInteger;

// Write 128-bit integer
BigInteger value = new BigInteger("123456789012345678901234567890");
offset += Borsh.write128(value, data, offset);

// Write optional 128-bit
offset += Borsh.write128Optional(value, data, offset);

// Read 128-bit integer
BigInteger[] values = Borsh.read128Vector(data, offset);

Strings

// Write string (UTF-8 encoded with length prefix)
String str = "Hello Solana";
offset += Borsh.write(str, data, offset);

// Write optional string
offset += Borsh.writeOptional(str, data, offset);
offset += Borsh.writeOptional((String) null, data, offset);

// Read string
String decoded = Borsh.readString(data, offset);

// Calculate length
int length = Borsh.len(str);  // 4 bytes (length) + UTF-8 bytes
int length = Borsh.lenOptional(str);  // 1 + len(str)

Arrays and Vectors

// Arrays (fixed length, no length prefix)
int[] array = {1, 2, 3, 4, 5};
offset += Borsh.writeArray(array, data, offset);

// Arrays with length checking
offset += Borsh.writeArrayChecked(
    array,
    5,  // expected length
    data,
    offset
);

// Vectors (dynamic length with 4-byte length prefix)
offset += Borsh.writeVector(array, data, offset);

// Calculate lengths
int arrayLen = Borsh.lenArray(array);   // element_size * count
int vectorLen = Borsh.lenVector(array); // 4 + lenArray(array)

Multi-dimensional Arrays

// 2D arrays
int[][] matrix = {{1, 2}, {3, 4}, {5, 6}};
offset += Borsh.writeArray(matrix, data, offset);

// 2D vectors
offset += Borsh.writeVector(matrix, data, offset);

// Read multi-dimensional
int[][] result = Borsh.readMultiDimensionintVector(data, offset);

// With fixed dimensions
int[][] result = Borsh.readMultiDimensionintVectorArray(
    3,  // fixed length
    data,
    offset
);

Byte Arrays (Special Case)

// Write raw bytes (no length prefix)
offset += Borsh.writeArray(bytes, data, offset);

// Write byte vector (with length prefix)
offset += Borsh.writeVector(bytes, data, offset);

// Write optional byte array
offset += Borsh.writeOptionalArray(bytes, data, offset);
offset += Borsh.writeOptionalVector(bytes, data, offset);

// Read byte vector
byte[] result = Borsh.readbyteVector(data, offset);

PublicKeys

import software.sava.core.accounts.PublicKey;

// PublicKeys are serialized as 32-byte arrays
PublicKey pubKey = PublicKey.fromBase58Encoded("...");
offset += pubKey.write(data, offset);

// Read PublicKey
PublicKey decoded = PublicKey.readPubKey(data, offset);

Custom Types

Implement the Borsh interface for custom serialization:
import software.sava.core.borsh.Borsh;
import software.sava.core.accounts.PublicKey;

public record MyData(
    PublicKey owner,
    long amount,
    boolean isActive
) implements Borsh {

    @Override
    public int write(byte[] data, int offset) {
        int i = offset;
        i += owner.write(data, i);
        i += Borsh.writeArray(new long[]{amount}, data, i);
        i += Borsh.write(isActive, data, i);
        return i - offset;
    }

    @Override
    public int l() {
        return 32 +      // PublicKey
               8 +       // long
               1;        // boolean
    }
    
    public static MyData read(byte[] data, int offset) {
        int i = offset;
        PublicKey owner = PublicKey.readPubKey(data, i);
        i += 32;
        
        long[] amounts = new long[1];
        i += Borsh.readArray(amounts, data, i);
        
        boolean isActive = data[i] == 1;
        
        return new MyData(owner, amounts[0], isActive);
    }
}

Compact U16 Encoding

Compact U16 encoding is used in Solana transactions to encode small integers efficiently.

Encoding Format

  • 1 byte: Values 0-127 (0x00-0x7F)
  • 2 bytes: Values 128-16,383 (0x80-0x3FFF)
  • 3 bytes: Values 16,384-262,143 (0x4000-0x3FFFF)

Encoding Operations

import software.sava.core.encoding.CompactU16Encoding;

// Encode to new byte array
int value = 1000;
byte[] encoded = CompactU16Encoding.encodeLength(value);

// Encode to existing buffer
int bytesWritten = CompactU16Encoding.encodeLength(
    buffer,
    offset,
    value
);

// Calculate byte length for value
int byteLen = CompactU16Encoding.getByteLen(value);
// 127 -> 1 byte
// 128 -> 2 bytes
// 16384 -> 3 bytes

Decoding Operations

// Decode from buffer
int value = CompactU16Encoding.decode(buffer, offset);

// Get byte length of encoded value
int byteLen = CompactU16Encoding.getByteLen(buffer, offset);

// Check if value uses continuation bit
boolean isSigned = CompactU16Encoding.signedByte(buffer[offset]);

Usage in Transactions

// Compact U16 is used for:
// - Number of accounts
// - Number of instructions
// - Instruction data length
// - Number of signatures

// Example: Encoding account count
int numAccounts = 25;
int offset = CompactU16Encoding.encodeLength(
    txBuffer,
    offset,
    numAccounts
);
Use compact encoding for transaction fields to minimize transaction size. Most transactions use far fewer than 128 accounts/instructions, so compact encoding saves 1-2 bytes per field.

Complete Example

Here’s a complete example showing all encoding types:
import software.sava.core.encoding.Base58;
import software.sava.core.encoding.CompactU16Encoding;
import software.sava.core.borsh.Borsh;
import software.sava.core.accounts.PublicKey;

public class EncodingExample {
    
    public static void main(String[] args) {
        // Base58 encoding
        PublicKey pubKey = PublicKey.fromBase58Encoded(
            "11111111111111111111111111111111"
        );
        String encoded = pubKey.toBase58();
        System.out.println("Base58: " + encoded);
        
        // Borsh serialization
        byte[] buffer = new byte[1000];
        int offset = 0;
        
        // Write various types
        offset += pubKey.write(buffer, offset);
        offset += Borsh.write("Hello Solana", buffer, offset);
        offset += Borsh.writeArray(new long[]{100, 200, 300}, buffer, offset);
        offset += Borsh.write(true, buffer, offset);
        
        // Compact U16 encoding
        int numItems = 42;
        offset += CompactU16Encoding.encodeLength(
            buffer,
            offset,
            numItems
        );
        
        System.out.println("Total bytes written: " + offset);
        
        // Read back
        int readOffset = 0;
        PublicKey decodedKey = PublicKey.readPubKey(buffer, readOffset);
        readOffset += 32;
        
        String decodedStr = Borsh.readString(buffer, readOffset);
        System.out.println("Decoded string: " + decodedStr);
    }
}

Best Practices

  1. Pre-calculate lengths: Use len() methods to allocate buffers of correct size
  2. Validate Base58: Check isBase58() before decoding untrusted input
  3. Use compact encoding: For transaction counts and small integers
  4. Check array bounds: Use writeArrayChecked() for fixed-size arrays
  5. Handle optionals: Remember that null is encoded as a single 0 byte

Performance Tips

  • Reuse buffers: Allocate once and reuse for encoding
  • Batch operations: Use array writes instead of individual element writes
  • Avoid allocations: Use mutable encoding methods when possible
  • Pre-size buffers: Calculate total length before allocating
All multi-byte integers in Borsh use little-endian encoding. When working with binary data from other sources, verify byte order to avoid corruption.

Build docs developers (and LLMs) love