30: Submit signed transactions through the Solana API

This commit is contained in:
2026-08-05 10:12:38 +02:00
parent ba376b6688
commit 2e61011a68
4 changed files with 127 additions and 3 deletions
@@ -10,7 +10,7 @@ import java.util.Map;
import java.util.Set;
/**
* Provides read-oriented access to the Solana blockchain.
* Provides generic access to the Solana blockchain.
*
* <p>This interface exposes the Solana operations needed by higher-level
* integrations. It can fetch native SOL balances, SPL token holdings, NFT-like
@@ -164,4 +164,22 @@ public interface SolanaBlockChain {
ΩSolanaProgramIdΩ programId,
Set<SolanaProgramAccountMemcmpFilter> filters
) throws IOException, InterruptedException;
/**
* Submits a complete signed transaction to the Solana blockchain.
*
* <p>The transaction must already contain all required signatures.
* This method does not sign or modify the transaction.</p>
*
* @param transaction the complete signed transaction to submit
* @return the transaction signature returned by Solana RPC
* @throws IllegalArgumentException if the serialized transaction is blank
* @throws IOException if the transaction cannot be submitted or Solana
* returns an RPC error
* @throws InterruptedException if the calling thread is interrupted while
* submitting the transaction
*/
ΩSolanaTransactionSignatureΩ sendTransaction(
SolanaSignedTransaction transaction
) throws IOException, InterruptedException;
}
@@ -0,0 +1,11 @@
package com.r35157.libs.solana;
/**
* A complete serialized Solana transaction containing all required signatures.
*
* @param serializedTransaction the signed transaction encoded as Base64
*/
public record SolanaSignedTransaction(
ΩBase64StringΩ serializedTransaction
) {
}
@@ -8,6 +8,7 @@ import com.r35157.libs.solana.SolanaAccountInfo;
import com.r35157.libs.solana.SolanaBlockChain;
import com.r35157.libs.solana.SolanaProgramAccountMemcmpFilter;
import com.r35157.libs.solana.SolanaProgramAddressSeed;
import com.r35157.libs.solana.SolanaSignedTransaction;
import com.r35157.libs.solana.valuetypes.SolanaProgramDerivedAddress;
import com.r35157.libs.solana.valuetypes.economic.SolanaSPLTokenProgram;
import com.r35157.libs.valuetypes.basic.MoneyAmount;
@@ -163,6 +164,13 @@ public final class CachedSolanaBlockChain implements SolanaBlockChain {
return result;
}
@Override
public ΩSolanaTransactionSignatureΩ sendTransaction(
SolanaSignedTransaction transaction
) throws IOException, InterruptedException {
return delegate.sendTransaction(transaction);
}
private synchronized Set<SolanaAccountInfo> loadProgramAccounts(
ObjectCacheKey key,
ΩSolanaProgramIdΩ programId,
@@ -2,12 +2,14 @@ package com.r35157.libs.solana.impl.ref;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.r35157.libs.solana.*;
import com.r35157.libs.solana.valuetypes.SolanaProgramDerivedAddress;
import com.r35157.libs.valuetypes.basic.MoneyAmount;
import com.r35157.libs.valuetypes.basic.WellKnownCurrencyTypes;
import com.r35157.libs.solana.valuetypes.economic.SolanaSPLTokenProgram;
import com.r35157.libs.valuetypes.basic.CurrencyType;
import com.r35157.libs.valuetypes.basic.MoneyAmount;
import com.r35157.libs.valuetypes.basic.WellKnownCurrencyTypes;
import java.io.IOException;
import java.math.BigDecimal;
@@ -392,6 +394,91 @@ public class SolanaBlockChainImpl implements SolanaBlockChain {
return Set.copyOf(accountInfos);
}
@Override
public ΩSolanaTransactionSignatureΩ sendTransaction(
SolanaSignedTransaction transaction
) throws IOException, InterruptedException {
String jsonBody = createSendTransactionBody(transaction);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(RPC_URL))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(jsonBody))
.build();
HttpResponse<String> response = sendThrottled(request);
if (response.statusCode() != 200) {
throw new IOException(
"Solana sendTransaction RPC call failed: HTTP "
+ response.statusCode()
+ "\n"
+ response.body()
);
}
JsonNode root = objectMapper.readTree(response.body());
JsonNode errorNode = root.get("error");
if (errorNode != null && !errorNode.isNull()) {
throw new IOException(
"Solana sendTransaction RPC error: "
+ errorNode.toPrettyString()
);
}
JsonNode resultNode = root.get("result");
if (resultNode == null
|| !resultNode.isTextual()
|| resultNode.asText().isBlank()) {
throw new IOException(
"Solana sendTransaction response did not contain "
+ "a transaction signature: "
+ response.body()
);
}
ΩSolanaTransactionSignatureΩ signature = resultNode.asText();
return signature;
}
private String createSendTransactionBody(
SolanaSignedTransaction transaction
) throws IOException {
if (transaction == null) {
throw new IllegalArgumentException(
"Signed transaction must not be null"
);
}
ΩBase64StringΩ serializedTransaction =
transaction.serializedTransaction();
if (serializedTransaction == null
|| serializedTransaction.isBlank()) {
throw new IllegalArgumentException(
"Serialized signed transaction must not be blank"
);
}
ObjectNode request = objectMapper.createObjectNode();
request.put("jsonrpc", "2.0");
request.put("id", 1);
request.put("method", "sendTransaction");
ArrayNode params = request.putArray("params");
params.add(serializedTransaction);
ObjectNode configuration = params.addObject();
configuration.put("encoding", "base64");
configuration.put("skipPreflight", false);
configuration.put("preflightCommitment", "confirmed");
return objectMapper.writeValueAsString(request);
}
private String createGetProgramAccountsBody(
ΩSolanaProgramIdΩ programId,
Set<SolanaProgramAccountMemcmpFilter> filters