Skip to content

Commit

Permalink
add docs & fix bugs.
Browse files Browse the repository at this point in the history
  • Loading branch information
overcat committed Jul 28, 2023
1 parent 880b21f commit e3e2490
Show file tree
Hide file tree
Showing 5 changed files with 206 additions and 39 deletions.
15 changes: 15 additions & 0 deletions src/main/java/org/stellar/sdk/AccountNotFoundException.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
14 changes: 0 additions & 14 deletions src/main/java/org/stellar/sdk/LedgerEntryNotFoundException.java

This file was deleted.

211 changes: 188 additions & 23 deletions src/main/java/org/stellar/sdk/SorobanServer.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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())
Expand All @@ -98,22 +120,46 @@ public TransactionBuilderAccount getAccount(String accountId)
List<GetLedgerEntriesResponse.LedgerEntryResult> 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());
long sequence = ledgerEntryData.getAccount().getSeqNum().getSequenceNumber().getInt64();
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.<Void, GetHealthResponse>sendRequest(
"getHealth", null, new TypeToken<SorobanRpcResponse<GetHealthResponse>>() {});
}

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<GetLedgerEntriesResponse.LedgerEntryResult> getContractData(
String contractId, SCVal key, Durability durability)
throws IOException, LedgerEntryNotFoundException {
throws IOException, SorobanRpcErrorResponse {

ContractDataDurability contractDataDurability;
switch (durability) {
Expand Down Expand Up @@ -145,13 +191,27 @@ public GetLedgerEntriesResponse.LedgerEntryResult getContractData(
List<GetLedgerEntriesResponse.LedgerEntryResult> 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<LedgerKey> keys) throws IOException {
/**
* Reads the current value of ledger entries directly.
*
* <p>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<LedgerKey> keys)
throws IOException, SorobanRpcErrorResponse {
List<String> xdrKeys =
keys.stream().map(SorobanServer::ledgerKeyToXdrBase64).collect(Collectors.toList());
GetLedgerEntriesRequest params = new GetLedgerEntriesRequest(xdrKeys);
Expand All @@ -161,29 +221,88 @@ public GetLedgerEntriesResponse getLedgerEntries(Collection<LedgerKey> keys) thr
new TypeToken<SorobanRpcResponse<GetLedgerEntriesResponse>>() {});
}

public GetTransactionResponse getTransaction(String hash) throws IOException {
/**
* Fetch the details of a submitted transaction.
*
* <p>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<SorobanRpcResponse<GetTransactionResponse>>() {});
}

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<SorobanRpcResponse<GetEventsResponse>>() {});
}

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.<Void, GetNetworkResponse>sendRequest(
"getNetwork", null, new TypeToken<SorobanRpcResponse<GetNetworkResponse>>() {});
}

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.<Void, GetLatestLedgerResponse>sendRequest(
"getLatestLedger", null, new TypeToken<SorobanRpcResponse<GetLatestLedgerResponse>>() {});
}

/**
* 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());
Expand All @@ -193,7 +312,34 @@ public SimulateTransactionResponse simulateTransaction(Transaction transaction)
new TypeToken<SorobanRpcResponse<SimulateTransactionResponse>>() {});
}

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.
*
* <p>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.
*
* <p>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());
Expand All @@ -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<SorobanRpcResponse<SendTransactionResponse>>() {});
}

private Transaction assembleTransaction(
Transaction transaction, SimulateTransactionResponse simulateTransactionResponse) {
if (!isSorobanTransaction(transaction)) {
Expand Down Expand Up @@ -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<SorobanRpcResponse<SendTransactionResponse>>() {});
}

private <T, R> R sendRequest(
String method, @Nullable T params, TypeToken<SorobanRpcResponse<R>> responseType)
throws IOException {
throws IOException, SorobanRpcErrorResponse {
String requestId = generateRequestId();
ResponseHandler<SorobanRpcResponse<R>> responseHandler = new ResponseHandler<>(responseType);
SorobanRpcRequest<T> sorobanRpcRequest = new SorobanRpcRequest<>(requestId, method, params);
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
3 changes: 2 additions & 1 deletion src/test/java/org/stellar/sdk/SorobanServerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

0 comments on commit e3e2490

Please sign in to comment.