Skip to content

Commit

Permalink
feat: add support for Horizon's transaction_async API. (#621)
Browse files Browse the repository at this point in the history
  • Loading branch information
overcat authored Jul 26, 2024
1 parent aa2f85f commit 0b9b51f
Show file tree
Hide file tree
Showing 7 changed files with 564 additions and 4 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ As this project is pre 1.0, breaking changes may happen for minor version bumps.
- refactor: `TransactionBuilder#TransactionBuilder(Transaction)` has been removed, because the TransactionBuilder constructed from the transaction may be inconsistent with what the user expects.
- fix: When calling `TransactionBuilder.build()`, the Soroban resource fee will be included in the `fee` of the built transaction.
- fix: fix the issue where invoking `SorobanServer.prepareTransaction` for transactions that have already set `SorobanData` could result in unexpected high fees.
- feat: add support for Soroban PRC's `getTransactions` and `getFeeStats` API.
- feat: add support for Horizon's 'transaction_async' API.

## 0.44.0
### Update
Expand Down
193 changes: 193 additions & 0 deletions src/main/java/org/stellar/sdk/Server.java
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,199 @@ public TransactionResponse submitTransaction(FeeBumpTransaction transaction) {
return submitTransaction(transaction, false);
}

/**
* Submits a base64 asynchronous transaction to the network. Unlike the synchronous version, which
* blocks and waits for the transaction to be ingested in Horizon, this endpoint relays the
* response from core directly back to the user.
*
* @param transactionXdr base64 encoded transaction envelope to submit to the network
* @return {@link SubmitTransactionAsyncResponse}
* @throws AccountRequiresMemoException when a transaction is trying to submit an operation to an
* account which requires a memo.
* @throws org.stellar.sdk.exception.NetworkException All the exceptions below are subclasses of
* NetworkError
* @throws org.stellar.sdk.exception.BadRequestException if the request fails due to a bad request
* (4xx)
* @throws org.stellar.sdk.exception.BadResponseException if the request fails due to a bad
* response from the server (5xx)
* @throws TooManyRequestsException if the request fails due to too many requests sent to the
* server
* @throws org.stellar.sdk.exception.RequestTimeoutException When Horizon returns a <code>Timeout
* </code> or connection timeout occurred
* @throws org.stellar.sdk.exception.UnknownResponseException if the server returns an unknown
* status code
* @throws org.stellar.sdk.exception.ConnectionErrorException When the request cannot be executed
* due to cancellation or connectivity problems, etc.
* @see <a
* href="https://developers.stellar.org/docs/data/horizon/api-reference/submit-async-transaction">Submit
* a Transaction Asynchronously</a>
*/
public SubmitTransactionAsyncResponse submitTransactionXdrAsync(String transactionXdr) {
HttpUrl transactionsURI = serverURI.newBuilder().addPathSegment("transactions_async").build();
RequestBody requestBody = new FormBody.Builder().add("tx", transactionXdr).build();
Request submitTransactionRequest =
new Request.Builder().url(transactionsURI).post(requestBody).build();
TypeToken<SubmitTransactionAsyncResponse> type =
new TypeToken<SubmitTransactionAsyncResponse>() {};

ResponseHandler<SubmitTransactionAsyncResponse> responseHandler = new ResponseHandler<>(type);
Response response;
try {
response = this.submitHttpClient.newCall(submitTransactionRequest).execute();
} catch (SocketTimeoutException e) {
throw new RequestTimeoutException(e);
} catch (IOException e) {
throw new ConnectionErrorException(e);
}
return responseHandler.handleResponse(response, true);
}

/**
* Submits a base64 asynchronous transaction to the network. Unlike the synchronous version, which
* blocks and waits for the transaction to be ingested in Horizon, this endpoint relays the
* response from core directly back to the user.
*
* @param transaction transaction to submit to the network
* @param skipMemoRequiredCheck set to true to skip memoRequiredCheck
* @return {@link TransactionResponse}
* @throws AccountRequiresMemoException when a transaction is trying to submit an operation to an
* account which requires a memo.
* @throws org.stellar.sdk.exception.NetworkException All the exceptions below are subclasses of
* NetworkError
* @throws org.stellar.sdk.exception.BadRequestException if the request fails due to a bad request
* (4xx)
* @throws org.stellar.sdk.exception.BadResponseException if the request fails due to a bad
* response from the server (5xx)
* @throws TooManyRequestsException if the request fails due to too many requests sent to the
* server
* @throws org.stellar.sdk.exception.RequestTimeoutException When Horizon returns a <code>Timeout
* </code> or connection timeout occurred
* @throws org.stellar.sdk.exception.UnknownResponseException if the server returns an unknown
* status code
* @throws org.stellar.sdk.exception.ConnectionErrorException When the request cannot be executed
* due to cancellation or connectivity problems, etc.
* @see <a
* href="https://developers.stellar.org/docs/data/horizon/api-reference/submit-async-transaction">Submit
* a Transaction Asynchronously</a>
*/
public SubmitTransactionAsyncResponse submitTransactionAsync(
Transaction transaction, boolean skipMemoRequiredCheck) {
if (!skipMemoRequiredCheck) {
checkMemoRequired(transaction);
}
return this.submitTransactionXdrAsync(transaction.toEnvelopeXdrBase64());
}

/**
* Submits a base64 asynchronous transaction to the network. Unlike the synchronous version, which
* blocks and waits for the transaction to be ingested in Horizon, this endpoint relays the
* response from core directly back to the user.
*
* @param transaction transaction to submit to the network
* @param skipMemoRequiredCheck set to true to skip memoRequiredCheck
* @return {@link SubmitTransactionAsyncResponse}
* @throws AccountRequiresMemoException when a transaction is trying to submit an operation to an
* account which requires a memo.
* @throws org.stellar.sdk.exception.NetworkException All the exceptions below are subclasses of
* NetworkError
* @throws org.stellar.sdk.exception.BadRequestException if the request fails due to a bad request
* (4xx)
* @throws org.stellar.sdk.exception.BadResponseException if the request fails due to a bad
* response from the server (5xx)
* @throws TooManyRequestsException if the request fails due to too many requests sent to the
* server
* @throws org.stellar.sdk.exception.RequestTimeoutException When Horizon returns a <code>Timeout
* </code> or connection timeout occurred
* @throws org.stellar.sdk.exception.UnknownResponseException if the server returns an unknown
* status code
* @throws org.stellar.sdk.exception.ConnectionErrorException When the request cannot be executed
* due to cancellation or connectivity problems, etc.
* @see <a
* href="https://developers.stellar.org/docs/data/horizon/api-reference/submit-async-transaction">Submit
* a Transaction Asynchronously</a>
*/
public SubmitTransactionAsyncResponse submitTransactionAsync(
FeeBumpTransaction transaction, boolean skipMemoRequiredCheck) {
if (!skipMemoRequiredCheck) {
checkMemoRequired(transaction.getInnerTransaction());
}
return this.submitTransactionXdrAsync(transaction.toEnvelopeXdrBase64());
}

/**
* Submits a base64 asynchronous transaction to the network. Unlike the synchronous version, which
* blocks and waits for the transaction to be ingested in Horizon, this endpoint relays the
* response from core directly back to the user.
*
* <p>This function will always check if the destination account requires a memo in the
* transaction as defined in <a
* href="https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0029.md"
* target="_blank">SEP-0029</a> If you want to skip this check, use {@link
* Server#submitTransactionAsync(Transaction, boolean)}.
*
* @param transaction transaction to submit to the network.
* @return {@link SubmitTransactionAsyncResponse}
* @throws AccountRequiresMemoException when a transaction is trying to submit an operation to an
* account which requires a memo.
* @throws org.stellar.sdk.exception.NetworkException All the exceptions below are subclasses of
* NetworkError
* @throws org.stellar.sdk.exception.BadRequestException if the request fails due to a bad request
* (4xx)
* @throws org.stellar.sdk.exception.BadResponseException if the request fails due to a bad
* response from the server (5xx)
* @throws TooManyRequestsException if the request fails due to too many requests sent to the
* server
* @throws org.stellar.sdk.exception.RequestTimeoutException When Horizon returns a <code>Timeout
* </code> or connection timeout occurred
* @throws org.stellar.sdk.exception.UnknownResponseException if the server returns an unknown
* status code
* @throws org.stellar.sdk.exception.ConnectionErrorException When the request cannot be executed
* due to cancellation or connectivity problems, etc.
* @see <a
* href="https://developers.stellar.org/docs/data/horizon/api-reference/submit-async-transaction">Submit
* a Transaction Asynchronously</a>
*/
public SubmitTransactionAsyncResponse submitTransactionAsync(Transaction transaction) {
return submitTransactionAsync(transaction, false);
}

/**
* Submits a base64 asynchronous transaction to the network. Unlike the synchronous version, which
* blocks and waits for the transaction to be ingested in Horizon, this endpoint relays the
* response from core directly back to the user.
*
* <p>This function will always check if the destination account requires a memo in the
* transaction as defined in <a
* href="https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0029.md"
* target="_blank">SEP-0029</a> If you want to skip this check, use {@link
* Server#submitTransactionAsync(Transaction, boolean)}.
*
* @param transaction transaction to submit to the network.
* @return {@link SubmitTransactionAsyncResponse}
* @throws AccountRequiresMemoException when a transaction is trying to submit an operation to an
* account which requires a memo.
* @throws org.stellar.sdk.exception.NetworkException All the exceptions below are subclasses of
* NetworkError
* @throws org.stellar.sdk.exception.BadRequestException if the request fails due to a bad request
* (4xx)
* @throws org.stellar.sdk.exception.BadResponseException if the request fails due to a bad
* response from the server (5xx)
* @throws TooManyRequestsException if the request fails due to too many requests sent to the
* server
* @throws org.stellar.sdk.exception.RequestTimeoutException When Horizon returns a <code>Timeout
* </code> or connection timeout occurred
* @throws org.stellar.sdk.exception.UnknownResponseException if the server returns an unknown
* status code
* @throws org.stellar.sdk.exception.ConnectionErrorException When the request cannot be executed
* due to cancellation or connectivity problems, etc.
* @see <a
* href="https://developers.stellar.org/docs/data/horizon/api-reference/submit-async-transaction">Submit
* a Transaction Asynchronously</a>
*/
public SubmitTransactionAsyncResponse submitTransactionAsync(FeeBumpTransaction transaction) {
return submitTransactionAsync(transaction, false);
}

private boolean hashMemoId(String muxedAccount) {
return StrKey.encodeToXDRMuxedAccount(muxedAccount).getDiscriminant()
== CryptoKeyType.KEY_TYPE_MUXED_ED25519;
Expand Down
17 changes: 16 additions & 1 deletion src/main/java/org/stellar/sdk/exception/BadRequestException.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import lombok.Getter;
import org.stellar.sdk.responses.Problem;
import org.stellar.sdk.responses.SubmitTransactionAsyncResponse;

/**
* Exception thrown when a bad request is made to the server. This typically indicates a client-side
Expand All @@ -12,15 +13,29 @@ public class BadRequestException extends NetworkException {
/** The parsed problem details, if available. */
private final Problem problem;

/**
* The parsed async transaction submission problem details.
*
* <p>This field is only present when the exception is thrown as a result of calling the "Submit
* Transaction Asynchronously" API endpoint and the server returned an error response. In other
* cases, it will be null.
*/
private final SubmitTransactionAsyncResponse submitTransactionAsyncProblem;

/**
* Constructs a new BadRequestException.
*
* @param code The HTTP status code of the response
* @param body The raw body of the response
* @param problem The parsed problem details, may be null if parsing failed
*/
public BadRequestException(int code, String body, Problem problem) {
public BadRequestException(
int code,
String body,
Problem problem,
SubmitTransactionAsyncResponse submitTransactionAsyncProblem) {
super("Bad Request.", code, body);
this.problem = problem;
this.submitTransactionAsyncProblem = submitTransactionAsyncProblem;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import lombok.Getter;
import org.stellar.sdk.responses.Problem;
import org.stellar.sdk.responses.SubmitTransactionAsyncResponse;

/**
* Exception thrown when a bad response is received from the server. This typically indicates a
Expand All @@ -12,15 +13,29 @@ public class BadResponseException extends NetworkException {
/** The parsed problem details, if available. */
private final Problem problem;

/**
* The parsed async transaction submission problem details.
*
* <p>This field is only present when the exception is thrown as a result of calling the "Submit
* Transaction Asynchronously" API endpoint and the server returned an error response. In other
* cases, it will be null.
*/
private final SubmitTransactionAsyncResponse submitTransactionAsyncProblem;

/**
* Constructs a new BadRequestException.
*
* @param code The HTTP status code of the response
* @param body The raw body of the response
* @param problem The parsed problem details, may be null if parsing failed
*/
public BadResponseException(int code, String body, Problem problem) {
public BadResponseException(
int code,
String body,
Problem problem,
SubmitTransactionAsyncResponse submitTransactionAsyncProblem) {
super("Bad Response.", code, body);
this.problem = problem;
this.submitTransactionAsyncProblem = submitTransactionAsyncProblem;
}
}
35 changes: 33 additions & 2 deletions src/main/java/org/stellar/sdk/requests/ResponseHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import org.stellar.sdk.exception.UnexpectedException;
import org.stellar.sdk.exception.UnknownResponseException;
import org.stellar.sdk.responses.Problem;
import org.stellar.sdk.responses.SubmitTransactionAsyncResponse;
import org.stellar.sdk.responses.gson.GsonSingleton;
import org.stellar.sdk.responses.gson.TypedResponse;

Expand Down Expand Up @@ -43,6 +44,24 @@ public ResponseHandler(TypeToken<T> type) {
* @throws BadResponseException If the response code is in the 5xx range
*/
public T handleResponse(final Response response) {
return handleResponse(response, false);
}

/**
* Handles the HTTP response and converts it to the appropriate object or throws exceptions based
* on the response status.
*
* @param response The HTTP response to handle
* @param submitTransactionAsync Only set it to true when calling {@link
* org.stellar.sdk.Server#submitTransactionXdrAsync(String)}.
* @return The parsed object of type T
* @throws TooManyRequestsException If the response code is 429 (Too Many Requests)
* @throws UnexpectedException If the response body is empty or there's an unexpected error
* reading the response
* @throws BadRequestException If the response code is in the 4xx range
* @throws BadResponseException If the response code is in the 5xx range
*/
public T handleResponse(final Response response, boolean submitTransactionAsync) {
try {
// Too Many Requests
if (response.code() == 429) {
Expand Down Expand Up @@ -84,18 +103,30 @@ public T handleResponse(final Response response) {
// Other errors
if (response.code() >= 400 && response.code() < 600) {
Problem problem = null;
SubmitTransactionAsyncResponse submitTransactionAsyncProblem = null;
try {
problem = GsonSingleton.getInstance().fromJson(content, Problem.class);
} catch (Exception e) {
// if we can't parse the response, we just ignore it
}

if (submitTransactionAsync) {
try {
submitTransactionAsyncProblem =
GsonSingleton.getInstance().fromJson(content, SubmitTransactionAsyncResponse.class);
} catch (Exception e) {
// if we can't parse the response, we just ignore it
}
}

if (response.code() < 500) {
// Codes in the 4xx range indicate an error that failed given the information provided
throw new BadRequestException(response.code(), content, problem);
throw new BadRequestException(
response.code(), content, problem, submitTransactionAsyncProblem);
} else {
// Codes in the 5xx range indicate an error with the Horizon server.
throw new BadResponseException(response.code(), content, problem);
throw new BadResponseException(
response.code(), content, problem, submitTransactionAsyncProblem);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package org.stellar.sdk.responses;

import com.google.gson.annotations.SerializedName;
import lombok.Value;
import org.stellar.sdk.Util;
import org.stellar.sdk.xdr.TransactionResult;

/**
* Represents the response from the "Submit a Transaction Asynchronously" endpoint of Horizon API.
*
* <p>See <a
* href="https://developers.stellar.org/docs/data/horizon/api-reference/submit-async-transaction">
* Submit Transaction Asynchronously</a>
*/
@Value
public class SubmitTransactionAsyncResponse {
String hash;

@SerializedName("tx_status")
TransactionStatus txStatus;

@SerializedName("errorResultXdr") // inconsistency
String errorResultXdr;

/**
* Parses the {@code errorResultXdr} field from a string to an {@link
* org.stellar.sdk.xdr.TransactionResult} object.
*
* @return the parsed {@link org.stellar.sdk.xdr.TransactionResult} object
*/
public TransactionResult parseErrorResultXdr() {
return Util.parseXdr(errorResultXdr, TransactionResult::fromXdrBase64);
}

public enum TransactionStatus {
ERROR,
PENDING,
DUPLICATE,
TRY_AGAIN_LATER,
}
}
Loading

0 comments on commit 0b9b51f

Please sign in to comment.