Skip to content

Commit

Permalink
Added support for calling Soroban RPC server.
Browse files Browse the repository at this point in the history
  • Loading branch information
overcat committed Jul 23, 2023
1 parent d6a7b60 commit 21c7c89
Show file tree
Hide file tree
Showing 18 changed files with 748 additions and 0 deletions.
310 changes: 310 additions & 0 deletions src/main/java/org/stellar/sdk/SorobanServer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,310 @@
package org.stellar.sdk;

import com.google.common.io.BaseEncoding;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.IOException;
import java.rmi.UnexpectedException;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import okhttp3.HttpUrl;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import org.stellar.sdk.requests.ClientIdentificationInterceptor;
import org.stellar.sdk.requests.ResponseHandler;
import org.stellar.sdk.requests.sorobanrpc.GetEventsRequest;
import org.stellar.sdk.requests.sorobanrpc.GetLedgerEntriesRequest;
import org.stellar.sdk.requests.sorobanrpc.GetTransactionRequest;
import org.stellar.sdk.requests.sorobanrpc.SendTransactionRequest;
import org.stellar.sdk.requests.sorobanrpc.SimulateTransactionRequest;
import org.stellar.sdk.requests.sorobanrpc.SorobanRpcErrorResponse;
import org.stellar.sdk.requests.sorobanrpc.SorobanRpcRequest;
import org.stellar.sdk.responses.sorobanrpc.GetEventsResponse;
import org.stellar.sdk.responses.sorobanrpc.GetHealthResponse;
import org.stellar.sdk.responses.sorobanrpc.GetLatestLedgerResponse;
import org.stellar.sdk.responses.sorobanrpc.GetLedgerEntriesResponse;
import org.stellar.sdk.responses.sorobanrpc.GetNetworkResponse;
import org.stellar.sdk.responses.sorobanrpc.GetTransactionResponse;
import org.stellar.sdk.responses.sorobanrpc.SendTransactionResponse;
import org.stellar.sdk.responses.sorobanrpc.SimulateTransactionResponse;
import org.stellar.sdk.responses.sorobanrpc.SorobanRpcResponse;
import org.stellar.sdk.xdr.ContractDataDurability;
import org.stellar.sdk.xdr.ContractEntryBodyType;
import org.stellar.sdk.xdr.LedgerEntry;
import org.stellar.sdk.xdr.LedgerEntryType;
import org.stellar.sdk.xdr.LedgerKey;
import org.stellar.sdk.xdr.SCVal;
import org.stellar.sdk.xdr.XdrDataInputStream;
import org.stellar.sdk.xdr.XdrDataOutputStream;

/**
* Main class used to connect to the Soroban-RPC instance and exposes an interface for requests to
* that instance.
*/
public class SorobanServer implements Closeable {
public static final int SUBMIT_TRANSACTION_TIMEOUT = 60; // seconds
private final HttpUrl serverURI;
private final OkHttpClient httpClient;
private final Gson gson = new Gson();

/**
* Creates a new SorobanServer instance.
*
* @param serverURI The URI of the Soroban-RPC instance to connect to.
*/
public SorobanServer(String serverURI) {
this(
serverURI,
new OkHttpClient.Builder()
.addInterceptor(new ClientIdentificationInterceptor())
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(SUBMIT_TRANSACTION_TIMEOUT, TimeUnit.SECONDS)
.retryOnConnectionFailure(true)
.build());
}

public SorobanServer(String serverURI, OkHttpClient httpClient) {
this.serverURI = HttpUrl.parse(serverURI);
this.httpClient = httpClient;
}

public TransactionBuilderAccount getAccount(String accountId, @Nullable String requestId)
throws IOException {
LedgerKey.LedgerKeyAccount ledgerKeyAccount =
new LedgerKey.LedgerKeyAccount.Builder()
.accountID(KeyPair.fromAccountId(accountId).getXdrAccountId())
.build();
LedgerKey ledgerKey =
new LedgerKey.Builder()
.account(ledgerKeyAccount)
.discriminant(LedgerEntryType.ACCOUNT)
.build();
SorobanRpcResponse<GetLedgerEntriesResponse> sorobanRpcResponse =
this.getLedgerEntries(Collections.singleton(ledgerKey), requestId);
List<GetLedgerEntriesResponse.LedgerEntryResult> entries =
sorobanRpcResponse.getResult().getEntries();
if (entries.isEmpty()) {
throw new SorobanRpcErrorResponse(
404, "Ledger entry not found. Key: " + ledgerKeyToXdrBase64(ledgerKey), "getAccount");
}
LedgerEntry.LedgerEntryData ledgerEntryData =
ledgerEntryDataFromXdrBase64(entries.get(0).getXdr());
long sequence = ledgerEntryData.getAccount().getSeqNum().getSequenceNumber().getInt64();
return new Account(accountId, sequence);
}

public SorobanRpcResponse<GetHealthResponse> getHealth(@Nullable String requestId)
throws IOException {
TypeToken<SorobanRpcResponse<GetHealthResponse>> responseType =
new TypeToken<SorobanRpcResponse<GetHealthResponse>>() {};
return this.<Void, GetHealthResponse>sendRequest("getHealth", null, responseType, requestId);
}

public SorobanRpcResponse<GetLedgerEntriesResponse.LedgerEntryResult> getContractData(
String contractId, SCVal key, Durability durability, @Nullable String requestId)
throws IOException {

ContractDataDurability contractDataDurability;
switch (durability) {
case TEMPORARY:
contractDataDurability = ContractDataDurability.TEMPORARY;
break;
case PERSISTENT:
contractDataDurability = ContractDataDurability.PERSISTENT;
break;
default:
throw new IllegalArgumentException("Invalid durability: " + durability);
}

Address address = new Address(contractId);

LedgerKey.LedgerKeyContractData ledgerKeyContractData =
new LedgerKey.LedgerKeyContractData.Builder()
.contract(address.toSCAddress())
.key(key)
.durability(contractDataDurability)
.bodyType(ContractEntryBodyType.DATA_ENTRY)
.build();
LedgerKey ledgerKey =
new LedgerKey.Builder()
.discriminant(LedgerEntryType.CONTRACT_DATA)
.contractData(ledgerKeyContractData)
.build();
SorobanRpcResponse<GetLedgerEntriesResponse> sorobanRpcResponse =
this.getLedgerEntries(Collections.singleton(ledgerKey), requestId);
List<GetLedgerEntriesResponse.LedgerEntryResult> entries =
sorobanRpcResponse.getResult().getEntries();
if (entries.isEmpty()) {
throw new SorobanRpcErrorResponse(
404,
"Ledger entry not found. Key: " + ledgerKeyToXdrBase64(ledgerKey),
"getContractData");
}

return new SorobanRpcResponse<>(
sorobanRpcResponse.getJsonRpc(),
sorobanRpcResponse.getId(),
entries.get(0),
sorobanRpcResponse.getError());
}

public SorobanRpcResponse<GetLedgerEntriesResponse> getLedgerEntries(
Collection<LedgerKey> keys, @Nullable String requestId) throws IOException {
List<String> xdrKeys =
keys.stream().map(this::ledgerKeyToXdrBase64).collect(Collectors.toList());
GetLedgerEntriesRequest params = new GetLedgerEntriesRequest(xdrKeys);
TypeToken<SorobanRpcResponse<GetLedgerEntriesResponse>> responseType =
new TypeToken<SorobanRpcResponse<GetLedgerEntriesResponse>>() {};
return this.sendRequest("getLedgerEntries", params, responseType, requestId);
}

public SorobanRpcResponse<GetTransactionResponse> getTransaction(
String hash, @Nullable String requestId) throws IOException {
TypeToken<SorobanRpcResponse<GetTransactionResponse>> responseType =
new TypeToken<SorobanRpcResponse<GetTransactionResponse>>() {};
GetTransactionRequest params = new GetTransactionRequest(hash);
return this.sendRequest("getTransaction", params, responseType, requestId);
}

public SorobanRpcResponse<GetEventsResponse> getEventsResponseSorobanRpcResponse(
GetEventsRequest getEventsRequest, @Nullable String requestId) throws IOException {
TypeToken<SorobanRpcResponse<GetEventsResponse>> responseType =
new TypeToken<SorobanRpcResponse<GetEventsResponse>>() {};
return this.sendRequest("getEvents", getEventsRequest, responseType, requestId);
}

public SorobanRpcResponse<GetNetworkResponse> getNetwork(@Nullable String requestId)
throws IOException {
TypeToken<SorobanRpcResponse<GetNetworkResponse>> responseType =
new TypeToken<SorobanRpcResponse<GetNetworkResponse>>() {};
return this.<Void, GetNetworkResponse>sendRequest("getNetwork", null, responseType, requestId);
}

public SorobanRpcResponse<GetLatestLedgerResponse> getLatestLedger(@Nullable String requestId)
throws IOException {
TypeToken<SorobanRpcResponse<GetLatestLedgerResponse>> responseType =
new TypeToken<SorobanRpcResponse<GetLatestLedgerResponse>>() {};
return this.<Void, GetLatestLedgerResponse>sendRequest(
"getLatestLedger", null, responseType, requestId);
}

public SorobanRpcResponse<SimulateTransactionResponse> simulateTransaction(
Transaction transaction, @Nullable String requestId) throws IOException {
// TODO: In the future, it may be necessary to consider FeeBumpTransaction.
SimulateTransactionRequest params =
new SimulateTransactionRequest(transaction.toEnvelopeXdrBase64());
TypeToken<SorobanRpcResponse<SimulateTransactionResponse>> responseType =
new TypeToken<SorobanRpcResponse<SimulateTransactionResponse>>() {};
return this.sendRequest("simulateTransaction", params, responseType, requestId);
}

public Transaction prepareTransaction(Transaction transaction, String networkPassphrase)
throws IOException {
SorobanRpcResponse<SimulateTransactionResponse> sorobanRpcResponse =
this.simulateTransaction(transaction, null);
if (sorobanRpcResponse.getResult().getError() != null) {
throw new UnexpectedException(""); // TODO
}

if (sorobanRpcResponse.getResult().getResults() == null
|| sorobanRpcResponse.getResult().getResults().size() != 1) {
throw new UnexpectedException(""); // TODO
}

return assembleTransaction(
transaction, networkPassphrase, sorobanRpcResponse.getResult().getResults().get(0));
}

private Transaction assembleTransaction(
Transaction transaction,
String networkPassphrase,
SimulateTransactionResponse.SimulateHostFunctionResult simulateHostFunctionResult)
throws IOException {
return null;
}

public SorobanRpcResponse<SendTransactionResponse> sendTransaction(
Transaction transaction, @Nullable String requestId) throws IOException {
// TODO: In the future, it may be necessary to consider FeeBumpTransaction.
SendTransactionRequest params = new SendTransactionRequest(transaction.toEnvelopeXdrBase64());
TypeToken<SorobanRpcResponse<SendTransactionResponse>> responseType =
new TypeToken<SorobanRpcResponse<SendTransactionResponse>>() {};
return this.sendRequest("sendTransaction", params, responseType, requestId);
}

private <T, R> SorobanRpcResponse<R> sendRequest(
String method,
@Nullable T params,
TypeToken<SorobanRpcResponse<R>> responseType,
@Nullable String requestId)
throws IOException {
if (requestId == null) {
requestId = generateRequestId();
}

ResponseHandler<SorobanRpcResponse<R>> responseHandler = new ResponseHandler<>(responseType);
SorobanRpcRequest<T> sorobanRpcRequest = new SorobanRpcRequest<>(requestId, method, params);
MediaType mediaType = MediaType.parse("application/json");
RequestBody requestBody =
RequestBody.create(gson.toJson(sorobanRpcRequest).getBytes(), mediaType);

Request request = new Request.Builder().url(this.serverURI).post(requestBody).build();
try (Response response = this.httpClient.newCall(request).execute()) {
SorobanRpcResponse<R> sorobanRpcResponse = responseHandler.handleResponse(response);
if (sorobanRpcResponse.getError() != null) {
SorobanRpcResponse.Error error = sorobanRpcResponse.getError();
throw new SorobanRpcErrorResponse(error.getCode(), error.getMessage(), error.getData());
}
return sorobanRpcResponse;
}
}

@Override
public void close() throws IOException {
this.httpClient.connectionPool().evictAll();
}

private String generateRequestId() {
return UUID.randomUUID().toString();
}

private String ledgerKeyToXdrBase64(LedgerKey ledgerKey) {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
XdrDataOutputStream xdrDataOutputStream = new XdrDataOutputStream(byteArrayOutputStream);
try {
ledgerKey.encode(xdrDataOutputStream);
} catch (IOException e) {
throw new IllegalArgumentException("invalid ledgerKey.", e);
}
BaseEncoding base64Encoding = BaseEncoding.base64();
return base64Encoding.encode(byteArrayOutputStream.toByteArray());
}

private LedgerEntry.LedgerEntryData ledgerEntryDataFromXdrBase64(String ledgerEntryData) {
BaseEncoding base64Encoding = BaseEncoding.base64();
byte[] bytes = base64Encoding.decode(ledgerEntryData);
ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);
XdrDataInputStream xdrInputStream = new XdrDataInputStream(inputStream);
try {
return LedgerEntry.LedgerEntryData.decode(xdrInputStream);
} catch (IOException e) {
throw new IllegalArgumentException("invalid ledgerEntryData: " + ledgerEntryData, e);
}
}

public enum Durability {
TEMPORARY,
PERSISTENT
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.stellar.sdk.requests.sorobanrpc;

import com.google.gson.annotations.SerializedName;

public enum EventFilterType {
@SerializedName("system")
SYSTEM,
@SerializedName("contract")
CONTRACT,
@SerializedName("diagnostic")
DIAGNOSTIC
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package org.stellar.sdk.requests.sorobanrpc;

import com.google.gson.annotations.SerializedName;
import java.util.Collection;
import lombok.Builder;
import lombok.NonNull;
import lombok.Singular;
import lombok.Value;

@Value
@Builder
public class GetEventsRequest {
@NonNull
@SerializedName("startLedger")
String startLedger;

@SerializedName("filters")
@Singular("filter")
Collection<EventFilter> filters;

@SerializedName("pagination")
PaginationOptions pagination;

@Value
@Builder
public static class PaginationOptions {
@SerializedName("limit")
Long limit;

@SerializedName("cursor")
String cursor;
}

@Builder
@Value
public static class EventFilter {
@SerializedName("type")
EventFilterType type;

@SerializedName("contractIds")
Collection<String> contractIds;

@Singular("topic")
@SerializedName("topics")
Collection<Collection<String>> topics;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.stellar.sdk.requests.sorobanrpc;

import com.google.gson.annotations.SerializedName;
import java.util.Collection;
import lombok.AllArgsConstructor;
import lombok.Value;

@AllArgsConstructor
@Value
public class GetLedgerEntriesRequest {
@SerializedName("keys")
Collection<String> keys;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.stellar.sdk.requests.sorobanrpc;

import com.google.gson.annotations.SerializedName;
import lombok.AllArgsConstructor;
import lombok.Value;

@Value
@AllArgsConstructor
public class GetTransactionRequest {
@SerializedName("hash")
String hash;
}
Loading

0 comments on commit 21c7c89

Please sign in to comment.