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