GitHub Repository
Dependencies
Each module’smodule-info.java is the list of record.
idl-clients-spl:java.net.http,software.sava.core,software.sava.rpcidl-clients-bundle: the above,software.sava.idl.clients.splandsystems.comodal.json_iterator
Supported Programs
Clients are generated from each program’s IDL, providing typed instruction builders and account (de)serialization, plus hand-written convenience clients where useful.| Module | Programs |
|---|---|
idl-clients-spl | Token, Token-2022, Associated Token Account, System, Stake, Stake Pool, Address Lookup Table, Memo, Compute Budget, signature verification precompiles, Attestation Service |
idl-clients-bundle | Jupiter, Kamino, Marginfi, Marinade, Metaplex, Meteora, Orca, Phoenix, Squads, CCTP, Exponent, Loopscale, Neutral Trade, Pyth, Switchboard, Wormhole |
Build Configuration
build.gradle.kts
dependencies {
implementation("software.sava:idl-clients-spl:$VERSION")
implementation("software.sava:idl-clients-bundle:$VERSION")
}
These modules are published to GitHub Packages only; the
quickstart shows the repository configuration.
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, 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 Legacy 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);
}
TxBuilder builds them from the same
generated Instruction objects; see the transaction guide for
construction and signing. The example above uses the legacy transaction factories, which
remain supported.
Transfer SOL
// splAccountClient, owner & feePayer from "Create a Program Client" above.
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, must sign
stakeAuthority, // base key the seed derives from
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 = StakeProgram.initialize(
solanaAccounts.invokedStakeProgram(),
stakeAccountPubKey,
authorized,
lockup
);
var delegateStakeIx = StakeProgram.delegateStake(
solanaAccounts.invokedStakeProgram(),
stakeAccountPubKey,
validatorVoteAccount,
stakeAuthority
);
var instructions = List.of(createStakeAccountIx, initializeIx, delegateStakeIx);
}
Durable Nonce Transactions
See the official Solana documentation for context on durable nonce transactions.
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()
);
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 sig = rpcClient.sendTransaction(base64Encoded).join();
System.out.println("https://explorer.solana.com/tx/" + sig);
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 sig = rpcClient.sendTransaction(base64Encoded).join();
System.out.println("https://explorer.solana.com/tx/" + sig);
}