-
Notifications
You must be signed in to change notification settings - Fork 160
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added support for calling Soroban RPC server.
- Loading branch information
Showing
18 changed files
with
748 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
12 changes: 12 additions & 0 deletions
12
src/main/java/org/stellar/sdk/requests/sorobanrpc/EventFilterType.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
47 changes: 47 additions & 0 deletions
47
src/main/java/org/stellar/sdk/requests/sorobanrpc/GetEventsRequest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
13 changes: 13 additions & 0 deletions
13
src/main/java/org/stellar/sdk/requests/sorobanrpc/GetLedgerEntriesRequest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
12 changes: 12 additions & 0 deletions
12
src/main/java/org/stellar/sdk/requests/sorobanrpc/GetTransactionRequest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
Oops, something went wrong.