Solana accounts are the foundation of the blockchain’s state storage. The Sava SDK provides a comprehensive set of interfaces and utilities for working with accounts, public keys, signers, and Program Derived Addresses (PDAs).
PublicKey Interface
The PublicKey interface represents a Solana account’s 32-byte address. It provides methods for creating, encoding, and verifying public keys.
Creating Public Keys
import software.sava.core.accounts.PublicKey;
// From Base58 encoded string
PublicKey pubKey = PublicKey.fromBase58Encoded("11111111111111111111111111111111");
// From byte array
byte[] keyBytes = new byte[32];
PublicKey pubKey = PublicKey.createPubKey(keyBytes);
// Read from buffer
PublicKey pubKey = PublicKey.readPubKey(buffer, offset);
Encoding and Conversion
Public keys can be converted between different formats:
// Convert to Base58 string
String base58 = pubKey.toBase58();
// Convert to byte array
byte[] bytes = pubKey.toByteArray();
// Convert to Java PublicKey
java.security.PublicKey javaPubKey = pubKey.toJavaPublicKey();
Constants
The PublicKey interface defines important constants:
int PUBLIC_KEY_LENGTH = 32; // 32 bytes
int MAX_SEED_LENGTH = 32; // Maximum seed length for derived addresses
int MAX_SEEDS = 16; // Maximum number of seeds
PublicKey NONE = ...; // All-zeros key (11111111111111111111111111111111)
The AccountMeta interface describes how an account is used in a transaction. It specifies whether an account is writable, a signer, the fee payer, or an invoked program.
import software.sava.core.accounts.meta.AccountMeta;
// Read-only account
AccountMeta readOnly = AccountMeta.createRead(publicKey);
// Writable account
AccountMeta writable = AccountMeta.createWrite(publicKey);
// Signer accounts
AccountMeta readOnlySigner = AccountMeta.createReadOnlySigner(publicKey);
AccountMeta writableSigner = AccountMeta.createWritableSigner(publicKey);
// Fee payer (automatically writable and signer)
AccountMeta feePayer = AccountMeta.createFeePayer(publicKey);
// Invoked program
AccountMeta program = AccountMeta.createInvoked(publicKey);
Each AccountMeta provides methods to check its properties:
boolean isSigner = accountMeta.signer();
boolean isWritable = accountMeta.write();
boolean isFeePayer = accountMeta.feePayer();
boolean isInvoked = accountMeta.invoked();
PublicKey key = accountMeta.publicKey();
When building instructions, accounts are automatically sorted according to Solana’s requirements: fee payer first, then signers, then writable accounts, and finally read-only accounts.
When multiple instructions reference the same account with different permissions, metadata can be merged:
AccountMeta merged = accountMeta1.merge(accountMeta2);
// The merged metadata will have the most permissive settings
Signers and Keypairs
The Signer interface represents an account that can sign transactions using its private key.
Generating Keypairs
import software.sava.core.accounts.Signer;
// Generate new random keypair
byte[] privateKey = Signer.generatePrivateKeyBytes();
Signer signer = Signer.createFromPrivateKey(privateKey);
// Generate complete keypair (private + public)
byte[] keyPair = Signer.generatePrivateKeyPairBytes();
Signer signer = Signer.createFromKeyPair(keyPair);
Loading Existing Keypairs
// From separate private and public keys
Signer signer = Signer.createFromKeyPair(publicKeyBytes, privateKeyBytes);
// From 64-byte keypair (32 private + 32 public)
Signer signer = Signer.createFromKeyPair(keyPairBytes);
// From Java PrivateKey and PublicKey
Signer signer = Signer.createFromKeyPair(publicKey, privateKey);
Signing Messages
// Sign a message
byte[] message = "Hello Solana".getBytes();
byte[] signature = signer.sign(message);
// Sign with offset and length
byte[] signature = signer.sign(message, offset, length);
// Sign in-place at specific position
int nextPosition = signer.sign(message, msgOffset, msgLen, outputPosition);
Key Validation
The SDK automatically validates keypairs when creating signers:
// Validates that public key matches private key
Signer.validateKeyPair(privateKey, expectedPublicKey);
// Validates complete keypair
Signer.validateKeyPair(keyPairBytes);
Always validate keypairs when loading them from external sources to ensure cryptographic consistency.
Program Derived Addresses (PDAs)
PDAs are deterministic addresses derived from a program ID and seeds. They provide a way to create accounts that can only be controlled by programs.
Finding PDAs
PDAs are found by searching for an address that is off the Ed25519 curve:
import software.sava.core.accounts.ProgramDerivedAddress;
import java.util.List;
// Define seeds
List<byte[]> seeds = List.of(
"metadata".getBytes(),
tokenMint.toByteArray(),
authority.toByteArray()
);
// Find PDA with bump seed
ProgramDerivedAddress pda = PublicKey.findProgramAddress(seeds, programId);
// Access the derived address
PublicKey pdaAddress = pda.publicKey();
int bumpSeed = pda.nonce();
List<byte[]> usedSeeds = pda.seeds();
Creating Program Addresses
If you already know the bump seed, you can directly create a program address:
// Create with known bump seed
List<byte[]> seedsWithBump = new ArrayList<>(seeds);
seedsWithBump.add(new byte[]{(byte) bumpSeed});
PublicKey programAddress = PublicKey.createProgramAddress(seedsWithBump, programId);
// Returns null if the address is on the curve
Seeds and Constraints
// Maximum values
int maxSeeds = PublicKey.MAX_SEEDS; // 16 seeds
int maxSeedLength = PublicKey.MAX_SEED_LENGTH; // 32 bytes per seed
The bump seed is searched from 255 down to 0. The first value that produces an off-curve address is used. This ensures deterministic derivation.
Account With Seed
For simpler use cases, you can create accounts derived from a base key and ASCII seed:
AccountWithSeed account = PublicKey.createOffCurveAccountWithAsciiSeed(
baseKey,
"my-seed",
programId
);
PublicKey derived = account.address();
byte[] seedWithBump = account.seed();
Signature Verification
The PublicKey interface provides methods to verify Ed25519 signatures:
// Verify signature
boolean valid = publicKey.verifySignature(message, signature);
// Verify with offset and length
boolean valid = publicKey.verifySignature(
message,
msgOffset,
msgLength,
signature
);
// Verify string message
boolean valid = publicKey.verifySignature("Hello Solana", signature);
// Static verification from byte arrays
boolean valid = PublicKey.verifySignature(
publicKeyBytes,
publicKeyOffset,
messageBytes,
msgOffset,
msgLength,
signatureBytes
);
Best Practices
- Reuse PublicKey instances:
PublicKey objects are immutable and can be safely reused
- Validate imported keys: Always validate keypairs from external sources
- Cache PDAs: Finding PDAs is computationally expensive; cache them when possible
- Use appropriate AccountMeta types: Choose the minimal permissions needed for each account
- Handle PDA derivation failures:
createProgramAddress returns null if unable to find an off-curve address
- Transactions - Learn how accounts are used in transactions
- Cryptography - Understand the underlying Ed25519 operations
- Encoding - See how public keys are encoded and serialized