From 2e61011a68f7e66cad2e6ce37931a45c7a7672eb Mon Sep 17 00:00:00 2001 From: Minimons Date: Sun, 19 Jul 2026 02:12:33 +0200 Subject: [PATCH] 30: Submit signed transactions through the Solana API --- .../r35157/libs/solana/SolanaBlockChain.tjava | 20 +++- .../libs/solana/SolanaSignedTransaction.tjava | 11 +++ .../impl/cached/CachedSolanaBlockChain.tjava | 8 ++ .../impl/ref/SolanaBlockChainImpl.tjava | 91 ++++++++++++++++++- 4 files changed, 127 insertions(+), 3 deletions(-) create mode 100644 src/main/tjava/com/r35157/libs/solana/SolanaSignedTransaction.tjava diff --git a/src/main/tjava/com/r35157/libs/solana/SolanaBlockChain.tjava b/src/main/tjava/com/r35157/libs/solana/SolanaBlockChain.tjava index 50b2ccf..7bd94d6 100644 --- a/src/main/tjava/com/r35157/libs/solana/SolanaBlockChain.tjava +++ b/src/main/tjava/com/r35157/libs/solana/SolanaBlockChain.tjava @@ -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. * *

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 filters ) throws IOException, InterruptedException; + + /** + * Submits a complete signed transaction to the Solana blockchain. + * + *

The transaction must already contain all required signatures. + * This method does not sign or modify the transaction.

+ * + * @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; } \ No newline at end of file diff --git a/src/main/tjava/com/r35157/libs/solana/SolanaSignedTransaction.tjava b/src/main/tjava/com/r35157/libs/solana/SolanaSignedTransaction.tjava new file mode 100644 index 0000000..b205ee6 --- /dev/null +++ b/src/main/tjava/com/r35157/libs/solana/SolanaSignedTransaction.tjava @@ -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 +) { +} \ No newline at end of file diff --git a/src/main/tjava/com/r35157/libs/solana/impl/cached/CachedSolanaBlockChain.tjava b/src/main/tjava/com/r35157/libs/solana/impl/cached/CachedSolanaBlockChain.tjava index 69340b3..d1adac0 100644 --- a/src/main/tjava/com/r35157/libs/solana/impl/cached/CachedSolanaBlockChain.tjava +++ b/src/main/tjava/com/r35157/libs/solana/impl/cached/CachedSolanaBlockChain.tjava @@ -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 loadProgramAccounts( ObjectCacheKey key, ΩSolanaProgramIdΩ programId, diff --git a/src/main/tjava/com/r35157/libs/solana/impl/ref/SolanaBlockChainImpl.tjava b/src/main/tjava/com/r35157/libs/solana/impl/ref/SolanaBlockChainImpl.tjava index 9f3c0c2..0140484 100644 --- a/src/main/tjava/com/r35157/libs/solana/impl/ref/SolanaBlockChainImpl.tjava +++ b/src/main/tjava/com/r35157/libs/solana/impl/ref/SolanaBlockChainImpl.tjava @@ -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 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 filters