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