diff --git a/src/main/java/org/stellar/sdk/AccountNotFoundException.java b/src/main/java/org/stellar/sdk/AccountNotFoundException.java new file mode 100644 index 000000000..fd028d2a8 --- /dev/null +++ b/src/main/java/org/stellar/sdk/AccountNotFoundException.java @@ -0,0 +1,15 @@ +package org.stellar.sdk; + +import lombok.Getter; + +/** Exception thrown when trying to load an account that doesn't exist on the Stellar network. */ +@Getter +public class AccountNotFoundException extends Exception { + // The account that was not found. + private final String accountId; + + public AccountNotFoundException(String accountId) { + super("Account not found, accountId: " + accountId); + this.accountId = accountId; + } +} diff --git a/src/main/java/org/stellar/sdk/LedgerEntryNotFoundException.java b/src/main/java/org/stellar/sdk/LedgerEntryNotFoundException.java deleted file mode 100644 index 8490306f9..000000000 --- a/src/main/java/org/stellar/sdk/LedgerEntryNotFoundException.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.stellar.sdk; - -public class LedgerEntryNotFoundException extends Exception { - private final String key; - - public LedgerEntryNotFoundException(String key) { - super("Ledger entry not found: " + key); - this.key = key; - } - - public String getKey() { - return key; - } -} diff --git a/src/main/java/org/stellar/sdk/SorobanServer.java b/src/main/java/org/stellar/sdk/SorobanServer.java index f347e77b5..c1cee014a 100644 --- a/src/main/java/org/stellar/sdk/SorobanServer.java +++ b/src/main/java/org/stellar/sdk/SorobanServer.java @@ -54,6 +54,7 @@ * Main class used to connect to the Soroban-RPC instance and exposes an interface for requests to * that instance. */ +@SuppressWarnings("KotlinInternalInJava") public class SorobanServer implements Closeable { private static final int SUBMIT_TRANSACTION_TIMEOUT = 60; // seconds private static final int CONNECT_TIMEOUT = 10; // seconds @@ -77,13 +78,34 @@ public SorobanServer(String serverURI) { .build()); } + /** + * Creates a new SorobanServer instance. + * + * @param serverURI The URI of the Soroban-RPC instance to connect to. + * @param httpClient The {@link OkHttpClient} instance to use for requests. + */ public SorobanServer(String serverURI, OkHttpClient httpClient) { this.serverURI = HttpUrl.parse(serverURI); this.httpClient = httpClient; } + /** + * Fetch a minimal set of current info about a Stellar account. Needed to get the current sequence + * number for the account, so you can build a successful transaction with {@link + * TransactionBuilder}. + * + * @param accountId The public address of the account to load. + * @return An {@link Account} object containing the sequence number and current state of the + * account. + * @throws IOException If the request could not be executed due to cancellation, a connectivity + * problem or timeout. Because networks can fail during an exchange, it is possible that the + * remote server accepted the request before the failure. + * @throws AccountNotFoundException If the account does not exist on the network. You may need to + * fund it first. + * @throws SorobanRpcErrorResponse If the Soroban-RPC instance returns an error response. + */ public TransactionBuilderAccount getAccount(String accountId) - throws IOException, LedgerEntryNotFoundException { + throws IOException, AccountNotFoundException, SorobanRpcErrorResponse { LedgerKey.LedgerKeyAccount ledgerKeyAccount = new LedgerKey.LedgerKeyAccount.Builder() .accountID(KeyPair.fromAccountId(accountId).getXdrAccountId()) @@ -98,7 +120,7 @@ public TransactionBuilderAccount getAccount(String accountId) List entries = getLedgerEntriesResponse.getEntries(); if (entries == null || entries.isEmpty()) { - throw new LedgerEntryNotFoundException(ledgerKeyToXdrBase64(ledgerKey)); + throw new AccountNotFoundException(accountId); } LedgerEntry.LedgerEntryData ledgerEntryData = ledgerEntryDataFromXdrBase64(entries.get(0).getXdr()); @@ -106,14 +128,38 @@ public TransactionBuilderAccount getAccount(String accountId) return new Account(accountId, sequence); } - public GetHealthResponse getHealth() throws IOException { + /** + * General node health check. + * + * @return A {@link GetHealthResponse} object containing the health check result. + * @throws IOException If the request could not be executed due to cancellation, a connectivity + * problem or timeout. Because networks can fail during an exchange, it is possible that the + * remote server accepted the request before the failure. + * @throws SorobanRpcErrorResponse If the Soroban-RPC instance returns an error response. + */ + public GetHealthResponse getHealth() throws IOException, SorobanRpcErrorResponse { return this.sendRequest( "getHealth", null, new TypeToken>() {}); } - public GetLedgerEntriesResponse.LedgerEntryResult getContractData( + /** + * Reads the current value of contract data ledger entries directly. + * + * @param contractId The contract ID containing the data to load. Encoded as Stellar Contract + * Address. e.g. "CCJZ5DGASBWQXR5MPFCJXMBI333XE5U3FSJTNQU7RIKE3P5GN2K2WYD5" + * @param key The key of the contract data to load. + * @param durability The "durability keyspace" that this ledger key belongs to, which is either + * {@link Durability#TEMPORARY} or '{@link Durability#PERSISTENT}'. + * @return A {@link GetLedgerEntriesResponse.LedgerEntryResult} object containing the ledger entry + * result. + * @throws IOException If the request could not be executed due to cancellation, a connectivity + * problem or timeout. Because networks can fail during an exchange, it is possible that the + * remote server accepted the request before the failure. + * @throws SorobanRpcErrorResponse If the Soroban-RPC instance returns an error response. + */ + public Optional getContractData( String contractId, SCVal key, Durability durability) - throws IOException, LedgerEntryNotFoundException { + throws IOException, SorobanRpcErrorResponse { ContractDataDurability contractDataDurability; switch (durability) { @@ -145,13 +191,27 @@ public GetLedgerEntriesResponse.LedgerEntryResult getContractData( List entries = getLedgerEntriesResponse.getEntries(); if (entries == null || entries.isEmpty()) { - throw new LedgerEntryNotFoundException(ledgerKeyToXdrBase64(ledgerKey)); + return Optional.empty(); } - - return entries.get(0); + GetLedgerEntriesResponse.LedgerEntryResult result = entries.get(0); + return Optional.of(result); } - public GetLedgerEntriesResponse getLedgerEntries(Collection keys) throws IOException { + /** + * Reads the current value of ledger entries directly. + * + *

Allows you to directly inspect the current state of contracts, contract's code, or any other + * ledger entries. + * + * @param keys The key of the contract data to load. + * @return A {@link GetLedgerEntriesResponse} object containing the current values. + * @throws IOException If the request could not be executed due to cancellation, a connectivity + * problem or timeout. Because networks can fail during an exchange, it is possible that the + * remote server accepted the request before the failure. + * @throws SorobanRpcErrorResponse If the Soroban-RPC instance returns an error response. + */ + public GetLedgerEntriesResponse getLedgerEntries(Collection keys) + throws IOException, SorobanRpcErrorResponse { List xdrKeys = keys.stream().map(SorobanServer::ledgerKeyToXdrBase64).collect(Collectors.toList()); GetLedgerEntriesRequest params = new GetLedgerEntriesRequest(xdrKeys); @@ -161,29 +221,88 @@ public GetLedgerEntriesResponse getLedgerEntries(Collection keys) thr new TypeToken>() {}); } - public GetTransactionResponse getTransaction(String hash) throws IOException { + /** + * Fetch the details of a submitted transaction. + * + *

When submitting a transaction, client should poll this to tell when the transaction has + * completed. + * + * @param hash The hash of the transaction to check. Encoded as a hex string. + * @return A {@link GetTransactionResponse} object containing the transaction status, result, and + * other details. + * @throws IOException If the request could not be executed due to cancellation, a connectivity + * problem or timeout. Because networks can fail during an exchange, it is possible that the + * remote server accepted the request before the failure. + * @throws SorobanRpcErrorResponse If the Soroban-RPC instance returns an error response. + */ + public GetTransactionResponse getTransaction(String hash) + throws IOException, SorobanRpcErrorResponse { GetTransactionRequest params = new GetTransactionRequest(hash); return this.sendRequest( "getTransaction", params, new TypeToken>() {}); } - public GetEventsResponse getEvents(GetEventsRequest getEventsRequest) throws IOException { + /** + * Fetches all events that match the given {@link GetEventsRequest}. + * + * @param getEventsRequest The {@link GetEventsRequest} to use for the request. + * @return A {@link GetEventsResponse} object containing the events that match the request. + * @throws IOException If the request could not be executed due to cancellation, a connectivity + * problem or timeout. Because networks can fail during an exchange, it is possible that the + * remote server accepted the request before the failure. + * @throws SorobanRpcErrorResponse If the Soroban-RPC instance returns an error response. + */ + public GetEventsResponse getEvents(GetEventsRequest getEventsRequest) + throws IOException, SorobanRpcErrorResponse { return this.sendRequest( "getEvents", getEventsRequest, new TypeToken>() {}); } - public GetNetworkResponse getNetwork() throws IOException { + /** + * Fetches metadata about the network which Soroban-RPC is connected to. + * + * @return A {@link GetNetworkResponse} object containing the network metadata. + * @throws IOException If the request could not be executed due to cancellation, a connectivity + * problem or timeout. Because networks can fail during an exchange, it is possible that the + * remote server accepted the request before the failure. + * @throws SorobanRpcErrorResponse If the Soroban-RPC instance returns an error response. + */ + public GetNetworkResponse getNetwork() throws IOException, SorobanRpcErrorResponse { return this.sendRequest( "getNetwork", null, new TypeToken>() {}); } - public GetLatestLedgerResponse getLatestLedger() throws IOException { + /** + * Fetches the latest ledger meta info from network which Soroban-RPC is connected to. + * + * @return A {@link GetLatestLedgerResponse} object containing the latest ledger meta info. + * @throws IOException If the request could not be executed due to cancellation, a connectivity + * problem or timeout. Because networks can fail during an exchange, it is possible that the + * remote server accepted the request before the failure. + * @throws SorobanRpcErrorResponse If the Soroban-RPC instance returns an error response. + */ + public GetLatestLedgerResponse getLatestLedger() throws IOException, SorobanRpcErrorResponse { return this.sendRequest( "getLatestLedger", null, new TypeToken>() {}); } + /** + * Submit a trial contract invocation to get back return values, expected ledger footprint, + * expected authorizations, and expected costs. + * + * @param transaction The transaction to simulate. It should include exactly one operation, which + * must be one of {@link InvokeHostFunctionOperation}, {@link + * BumpFootprintExpirationOperation}, or {@link RestoreFootprintOperation}. Any provided + * footprint will be ignored. + * @return A {@link SimulateTransactionResponse} object containing the cost, footprint, + * result/auth requirements (if applicable), and error of the transaction. + * @throws IOException If the request could not be executed due to cancellation, a connectivity + * problem or timeout. Because networks can fail during an exchange, it is possible that the + * remote server accepted the request before the failure. + * @throws SorobanRpcErrorResponse If the Soroban-RPC instance returns an error response. + */ public SimulateTransactionResponse simulateTransaction(Transaction transaction) - throws IOException { + throws IOException, SorobanRpcErrorResponse { // TODO: In the future, it may be necessary to consider FeeBumpTransaction. SimulateTransactionRequest params = new SimulateTransactionRequest(transaction.toEnvelopeXdrBase64()); @@ -193,7 +312,34 @@ public SimulateTransactionResponse simulateTransaction(Transaction transaction) new TypeToken>() {}); } - public Transaction prepareTransaction(Transaction transaction) throws IOException { + /** + * Submit a trial contract invocation, first run a simulation of the contract invocation as + * defined on the incoming transaction, and apply the results to a new copy of the transaction + * which is then returned. Setting the ledger footprint and authorization, so the resulting + * transaction is ready for signing & sending. + * + *

The returned transaction will also have an updated fee that is the sum of fee set on + * incoming transaction with the contract resource fees estimated from simulation. It is advisable + * to check the fee on returned transaction and validate or take appropriate measures for + * interaction with user to confirm it is acceptable. + * + *

You can call the {@link SorobanServer#simulateTransaction} method directly first if you want + * to inspect estimated fees for a given transaction in detail first, if that is of importance. + * + * @param transaction The transaction to prepare. It should include exactly one operation, which + * must be one of {@link InvokeHostFunctionOperation}, {@link + * BumpFootprintExpirationOperation}, or {@link RestoreFootprintOperation}. Any provided + * footprint will be ignored. + * @return Returns a copy of the {@link Transaction}, with the expected authorizations (in the + * case of invocation) and ledger footprint added. The transaction fee will also automatically + * be padded with the contract's minimum resource fees discovered from the simulation. + * @throws IOException If the request could not be executed due to cancellation, a connectivity + * problem or timeout. Because networks can fail during an exchange, it is possible that the + * remote server accepted the request before the failure. + * @throws SorobanRpcErrorResponse If the Soroban-RPC instance returns an error response. + */ + public Transaction prepareTransaction(Transaction transaction) + throws IOException, SorobanRpcErrorResponse { SimulateTransactionResponse simulateTransactionResponse = this.simulateTransaction(transaction); if (simulateTransactionResponse.getError() != null) { throw new PrepareTransactionException(simulateTransactionResponse.getError()); @@ -206,6 +352,28 @@ public Transaction prepareTransaction(Transaction transaction) throws IOExceptio return assembleTransaction(transaction, simulateTransactionResponse); } + /** + * Submit a real transaction to the Stellar network. This is the only way to make changes + * "on-chain". Unlike Horizon, Soroban-RPC does not wait for transaction completion. It simply + * validates the transaction and enqueues it. Clients should call {@link + * SorobanServer#getTransaction} to learn about transaction's status. + * + * @param transaction The transaction to submit. + * @return A {@link SendTransactionResponse} object containing some details about the transaction + * that was submitted. + * @throws IOException If the request could not be executed due to cancellation, a connectivity + * problem or timeout. Because networks can fail during an exchange, it is possible that the + * remote server accepted the request before the failure. + * @throws SorobanRpcErrorResponse If the Soroban-RPC instance returns an error response. + */ + public SendTransactionResponse sendTransaction(Transaction transaction) + throws IOException, SorobanRpcErrorResponse { + // TODO: In the future, it may be necessary to consider FeeBumpTransaction. + SendTransactionRequest params = new SendTransactionRequest(transaction.toEnvelopeXdrBase64()); + return this.sendRequest( + "sendTransaction", params, new TypeToken>() {}); + } + private Transaction assembleTransaction( Transaction transaction, SimulateTransactionResponse simulateTransactionResponse) { if (!isSorobanTransaction(transaction)) { @@ -251,16 +419,9 @@ private Transaction assembleTransaction( transaction.getNetwork()); } - public SendTransactionResponse sendTransaction(Transaction transaction) throws IOException { - // TODO: In the future, it may be necessary to consider FeeBumpTransaction. - SendTransactionRequest params = new SendTransactionRequest(transaction.toEnvelopeXdrBase64()); - return this.sendRequest( - "sendTransaction", params, new TypeToken>() {}); - } - private R sendRequest( String method, @Nullable T params, TypeToken> responseType) - throws IOException { + throws IOException, SorobanRpcErrorResponse { String requestId = generateRequestId(); ResponseHandler> responseHandler = new ResponseHandler<>(responseType); SorobanRpcRequest sorobanRpcRequest = new SorobanRpcRequest<>(requestId, method, params); @@ -337,6 +498,10 @@ private static boolean isSorobanTransaction(Transaction transaction) { || op instanceof RestoreFootprintOperation; } + /** + * Represents the "durability keyspace" that this ledger key belongs to, check {@link + * SorobanServer#getContractData} for more details. + */ public enum Durability { TEMPORARY, PERSISTENT diff --git a/src/main/java/org/stellar/sdk/requests/sorobanrpc/SorobanRpcErrorResponse.java b/src/main/java/org/stellar/sdk/requests/sorobanrpc/SorobanRpcErrorResponse.java index 954cc5987..b5b685974 100644 --- a/src/main/java/org/stellar/sdk/requests/sorobanrpc/SorobanRpcErrorResponse.java +++ b/src/main/java/org/stellar/sdk/requests/sorobanrpc/SorobanRpcErrorResponse.java @@ -3,7 +3,7 @@ import lombok.Getter; @Getter -public class SorobanRpcErrorResponse extends RuntimeException { +public class SorobanRpcErrorResponse extends Exception { private final Integer code; private final String message; diff --git a/src/test/java/org/stellar/sdk/SorobanServerTest.java b/src/test/java/org/stellar/sdk/SorobanServerTest.java index d84e55177..da2e44d25 100644 --- a/src/test/java/org/stellar/sdk/SorobanServerTest.java +++ b/src/test/java/org/stellar/sdk/SorobanServerTest.java @@ -11,13 +11,14 @@ import org.jetbrains.annotations.NotNull; import org.junit.Test; import org.stellar.sdk.requests.sorobanrpc.GetTransactionRequest; +import org.stellar.sdk.requests.sorobanrpc.SorobanRpcErrorResponse; import org.stellar.sdk.requests.sorobanrpc.SorobanRpcRequest; public class SorobanServerTest { private final Gson gson = new Gson(); @Test - public void testGetTransaction() throws IOException { + public void testGetTransaction() throws IOException, SorobanRpcErrorResponse { String hash = "06dd9ee70bf93bbfe219e2b31363ab5a0361cc6285328592e4d3d1fed4c9025c"; String json = "{\n"