Documentation Index
Fetch the complete documentation index at: https://sava.software/llms.txt
Use this file to discover all available pages before exploring further.
GitHub Repository
Dependencies
- java.net.http
- org.bouncycastle.provider
- systems.comodal.json_iterator
- software.sava.core
- software.sava.rpc
Usage Examples
Create a Program Client
The idl-clients-spl module, used throughout the examples below, providesSPLClient and SPLAccountClient for working with tokens, associated token accounts,
stake accounts, address lookup tables, etc.
var solanaAccounts = SolanaAccounts.MAIN_NET;
var splClient = SPLClient.createClient(solanaAccounts);
// Create an account-specific client bound to an owner and fee payer
var owner = PublicKey.fromBase58Encoded(/* your public key */);
var feePayer = AccountMeta.createFeePayer(/* fee payer public key */);
var splAccountClient = splClient.createAccountClient(owner, feePayer);
Typical Transaction Flow
Signer signer = ...; // load private key
var feePayer = AccountMeta.createFeePayer(signer.publicKey());
var solanaAccounts = SolanaAccounts.MAIN_NET;
try (var httpClient = HttpClient.newHttpClient()) {
var rpcClient = SolanaRpcClient.build()
.endpoint(SolanaNetwork.MAIN_NET.getEndpoint())
.httpClient(httpClient)
.createClient();
List<Instruction> instructions = List.of(
ComputeBudgetUtil.MAX_COMPUTE_BUDGET_IX,
ComputeBudgetProgram.setComputeUnitPrice(
solanaAccounts.invokedComputeBudgetProgram(),
0
)
// TODO: Create and add relevant instructions
);
var simulationTransaction = Transaction.createTx(feePayer, instructions);
var simulationFuture = rpcClient.simulateTransaction(simulationTransaction);
// Apply a user-provided fee or fetch an estimate from a service such as Helius' Fee API.
var cuPriceIx = ComputeBudgetProgram.setComputeUnitPrice(
solanaAccounts.invokedComputeBudgetProgram(),
42
);
var simulationResult = simulationFuture.join();
var transaction = simulationTransaction
.replaceInstruction(0,
ComputeBudgetProgram.setComputeUnitLimit(
solanaAccounts.invokedComputeBudgetProgram(),
// CU usage can be dynamic and may need to be increased.
simulationResult.unitsConsumed().getAsInt()
)
)
.replaceInstruction(1, cuPriceIx);
transaction.setRecentBlockHash(simulationResult.replacementBlockHash().blockhash());
transaction.sign(signer);
var base64Encoded = transaction.base64EncodeToString();
var sig = rpcClient.sendTransaction(base64Encoded).join();
System.out.println(sig);
}
Transfer SOL
var transferTo = PublicKey.fromBase58Encoded(/* recipient */);
var transferIx = splAccountClient.transferSolLamports(transferTo, 42);
Associated Token Accounts
var mint = PublicKey.fromBase58Encoded(/* token mint */);
// Find the ATA for the standard Token Program
ProgramDerivedAddress ata = splClient.findATA(owner, mint);
// Create the ATA (idempotent - won't fail if it already exists)
var tokenProgram = solanaAccounts.tokenProgram();
Instruction createAtaIx = splAccountClient.createATAForOwnerFundedByFeePayer(
true, // idempotent
ata.publicKey(),
mint,
tokenProgram
);
Token Transfers
var fromTokenAccount = PublicKey.fromBase58Encoded(/* source token account */);
var toTokenAccount = PublicKey.fromBase58Encoded(/* destination token account */);
var tokenMint = PublicKey.fromBase58Encoded(/* mint */);
var invokedTokenProgram = solanaAccounts.invokedTokenProgram();
long amount = 1_000_000;
int decimals = 6;
Instruction transferIx = splAccountClient.transferTokenChecked(
invokedTokenProgram,
fromTokenAccount,
toTokenAccount,
amount,
decimals,
tokenMint
);
Stake Accounts
Signer signer = ...;
var stakeAuthority = signer.publicKey();
var feePayer = AccountMeta.createFeePayer(stakeAuthority);
var validatorVoteAccount = PublicKey.fromBase58Encoded(/* vote account */);
long stakeLamports = LamportDecimal.fromBigDecimal(BigDecimal.ONE).longValue();
var solanaAccounts = SolanaAccounts.MAIN_NET;
try (var httpClient = HttpClient.newHttpClient()) {
var rpcClient = SolanaRpcClient.build()
.endpoint(SolanaNetwork.MAIN_NET.getEndpoint())
.httpClient(httpClient)
.createClient();
long minRent = rpcClient.getMinimumBalanceForRentExemption(StakeAccount.BYTES).join();
var seed = ZonedDateTime.now(ZoneOffset.UTC).toString();
var stakeAccountPubKey = PublicKey.createOffCurveAccountWithAsciiSeed(
stakeAuthority,
seed,
solanaAccounts.stakeProgram()
).publicKey();
var createStakeAccountIx = SystemProgram.createAccountWithSeed(
solanaAccounts.invokedSystemProgram(),
stakeAuthority, // payer
stakeAccountPubKey, // new account
stakeAuthority, // base account (signer)
stakeAuthority, // base
seed,
minRent + stakeLamports,
StakeAccount.BYTES,
solanaAccounts.stakeProgram()
);
var authorized = new Authorized(stakeAuthority, stakeAuthority);
var lockup = new Lockup(new UnixTimestamp(0), new Epoch(0), PublicKey.NONE);
var initializeIx = SolanaStakeInterfaceProgram.initialize(
solanaAccounts.invokedStakeProgram(),
solanaAccounts,
stakeAccountPubKey,
authorized,
lockup
);
var delegateStakeIx = SolanaStakeInterfaceProgram.delegateStake(
solanaAccounts.invokedStakeProgram(),
solanaAccounts,
stakeAccountPubKey,
validatorVoteAccount,
solanaAccounts.stakeConfig(),
stakeAuthority
);
var instructions = List.of(createStakeAccountIx, initializeIx, delegateStakeIx);
}
Create & Extend Lookup Table
var solanaAccounts = SolanaAccounts.MAIN_NET;
var splClient = SPLClient.createClient(solanaAccounts);
var splAccountClient = splClient.createAccountClient(owner, feePayer);
var newAccounts = List.of(
PublicKey.fromBase58Encoded(""),
PublicKey.fromBase58Encoded(""),
PublicKey.fromBase58Encoded("")
);
long recentSlot = rpcClient.getSlot().join();
var lookupTablePDA = splAccountClient.findLookupTableAddress(recentSlot);
var createLookupTableIx = splAccountClient.createLookupTable(lookupTablePDA, recentSlot);
var extendTableIx = splAccountClient.extendLookupTable(lookupTablePDA.publicKey(), newAccounts);
var instructions = List.of(createLookupTableIx, extendTableIx);
Durable Nonce Transactions
See the official Solana documentation for context on durable nonce transactions.
SystemProgram and NonceAccount types under software.sava.idl.clients.spl.system provide everything needed
to create, initialize, and consume durable nonce accounts without depending on solana-programs.
Create & Initialize Nonce Account
Signer signer = ...;
var rpcEndpoint = SolanaNetwork.MAIN_NET.getEndpoint();
try (var httpClient = HttpClient.newHttpClient()) {
var rpcClient = SolanaRpcClient.build()
.endpoint(rpcEndpoint)
.httpClient(httpClient)
.createClient();
var blockHashFuture = rpcClient.getLatestBlockHash();
var minRentFuture = rpcClient.getMinimumBalanceForRentExemption(NonceAccount.BYTES);
var solanaAccounts = SolanaAccounts.MAIN_NET;
var seed = "nonce";
var nonceAccountWithSeed = PublicKey.createOffCurveAccountWithAsciiSeed(
signer.publicKey(),
seed,
solanaAccounts.systemProgram()
);
var initializeNonceAccountIx = SystemProgram.initializeNonceAccount(
solanaAccounts.invokedSystemProgram(),
solanaAccounts,
nonceAccountWithSeed.publicKey(),
signer.publicKey()
);
System.out.format("""
Fetching block hash and minimum rent to create nonce account %s with authority %s.
""",
nonceAccountWithSeed.publicKey(),
signer.publicKey()
);
long minRent = minRentFuture.join();
var createNonceAccountIx = SystemProgram.createAccountWithSeed(
solanaAccounts.invokedSystemProgram(),
signer.publicKey(), // payer
nonceAccountWithSeed.publicKey(), // new account
signer.publicKey(), // base account (signer)
signer.publicKey(), // base
seed,
minRent,
NonceAccount.BYTES,
solanaAccounts.systemProgram()
);
var instructions = List.of(createNonceAccountIx, initializeNonceAccountIx);
var transaction = Transaction.createTx(signer.publicKey(), instructions);
var blockHash = blockHashFuture.join().blockHash();
transaction.setRecentBlockHash(blockHash);
transaction.sign(signer);
var base64Encoded = transaction.base64EncodeToString();
var sendTransactionFuture = rpcClient.sendTransaction(base64Encoded);
System.out.format("""
Creating nonce account %s
https://explorer.solana.com/tx/%s
""",
nonceAccountWithSeed.publicKey(),
transaction.getBase58Id()
);
var sig = sendTransactionFuture.join();
System.out.format("""
Confirmed transaction %s
https://solscan.io/account/%s
""",
sig,
nonceAccountWithSeed.publicKey()
);
var nonceAccountInfo = rpcClient.getAccountInfo(nonceAccountWithSeed.publicKey()).join();
var nonceAccount = NonceAccount.read(nonceAccountInfo);
System.out.println(nonceAccount);
}
Create & Send Durable Nonce Transaction
Signer signer = ...;
var nonceAccountKey = PublicKey.fromBase58Encoded("");
var sendToKey = PublicKey.fromBase58Encoded("");
var transferSOL = new BigDecimal("0.0");
var solanaAccounts = SolanaAccounts.MAIN_NET;
var rpcEndpoint = SolanaNetwork.MAIN_NET.getEndpoint();
try (var httpClient = HttpClient.newHttpClient()) {
var rpcClient = SolanaRpcClient.build()
.endpoint(rpcEndpoint)
.httpClient(httpClient)
.createClient();
var nonceAccountInfo = rpcClient.getAccountInfo(nonceAccountKey).join();
var nonceAccount = NonceAccount.read(nonceAccountInfo);
System.out.println(nonceAccount);
var advanceNonceIx = nonceAccount.advanceNonceAccount(solanaAccounts);
var transferIx = SystemProgram.transferSol(
solanaAccounts.invokedSystemProgram(),
signer.publicKey(),
sendToKey,
LamportDecimal.fromBigDecimal(transferSOL).longValue()
);
var instructions = List.of(advanceNonceIx, transferIx);
var transaction = Transaction.createTx(signer.publicKey(), instructions);
nonceAccount.setNonce(transaction);
transaction.sign(signer);
var base64Encoded = transaction.base64EncodeToString();
var sendTransactionFuture = rpcClient.sendTransaction(base64Encoded);
System.out.format("""
Transferring %s SOL from %s to %s.
https://explorer.solana.com/tx/%s
""",
transferSOL.toPlainString(), signer.publicKey(), sendToKey, transaction.getBase58Id()
);
var sig = sendTransactionFuture.join();
System.out.println("Confirmed transaction "+ sig);
}