diff --git a/clients/horizonclient/internal.go b/clients/horizonclient/internal.go index 9dc6052263..bde8f2bd76 100644 --- a/clients/horizonclient/internal.go +++ b/clients/horizonclient/internal.go @@ -1,13 +1,16 @@ package horizonclient import ( + "bytes" "encoding/json" + "io" "net/http" "net/url" "strconv" "strings" "time" + "github.com/stellar/go/protocols/horizon" "github.com/stellar/go/support/clock" "github.com/stellar/go/support/errors" ) @@ -27,10 +30,11 @@ func decodeResponse(resp *http.Response, object interface{}, horizonUrl string, } setCurrentServerTime(u.Hostname(), resp.Header["Date"], clock) - // While this part of code assumes that any error < 200 or error >= 300 is a Horizon problem, it is not - // true for the response from /transactions_async endpoint which does give these codes for certain responses - // from core. - if !(resp.StatusCode >= 200 && resp.StatusCode < 300) && (resp.Request == nil || resp.Request.URL == nil || resp.Request.URL.Path != "/transactions_async") { + if isStatusCodeAnError(resp.StatusCode) { + if isAsyncTxSubRequest(resp) { + return decodeAsyncTxSubResponse(resp, object) + } + horizonError := &Error{ Response: resp, } @@ -47,6 +51,44 @@ func decodeResponse(resp *http.Response, object interface{}, horizonUrl string, return } +func isStatusCodeAnError(statusCode int) bool { + return !(statusCode >= 200 && statusCode < 300) +} + +func isAsyncTxSubRequest(resp *http.Response) bool { + return resp.Request != nil && resp.Request.URL != nil && resp.Request.URL.Path == "/transactions_async" +} + +func decodeAsyncTxSubResponse(resp *http.Response, object interface{}) error { + // We need to read the entire body in order to create 2 decoders later. + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + return errors.Wrap(err, "error reading response body") + } + + // The first decoder converts the response to AsyncTransactionSubmissionResponse and checks + // the hash of the transaction. If the response was not a valid one, the hash will be empty. + decoder1 := json.NewDecoder(bytes.NewReader(bodyBytes)) + err = decoder1.Decode(&object) + if err != nil { + return errors.Wrap(err, "error decoding response") + } + if asyncResp, ok := object.(*horizon.AsyncTransactionSubmissionResponse); ok && asyncResp.Hash != "" { + return nil + } + + // Create a new reader for the second decoding. The second decoder decodes to Horizon.Problem object. + decoder2 := json.NewDecoder(bytes.NewReader(bodyBytes)) + horizonErr := Error{ + Response: resp, + } + err = decoder2.Decode(&horizonErr.Problem) + if err != nil { + return errors.Wrap(err, "error decoding horizon error") + } + return horizonErr +} + // countParams counts the number of parameters provided func countParams(params ...interface{}) int { counter := 0 diff --git a/services/horizon/internal/integration/txsub_async_test.go b/services/horizon/internal/integration/txsub_async_test.go index 2d16e4d4ea..4d9ff8e906 100644 --- a/services/horizon/internal/integration/txsub_async_test.go +++ b/services/horizon/internal/integration/txsub_async_test.go @@ -7,6 +7,7 @@ import ( "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/stellar/go/clients/horizonclient" "github.com/stellar/go/protocols/horizon" @@ -131,6 +132,31 @@ func TestAsyncTxSub_SubmissionTryAgainLater(t *testing.T) { }) } +func TestAsyncTxSub_TransactionMalformed(t *testing.T) { + itest := integration.NewTest(t, integration.Config{ + EnableSorobanRPC: true, + HorizonEnvironment: map[string]string{ + "MAX_HTTP_REQUEST_SIZE": "1800", + }, + }) + master := itest.Master() + + // establish which account will be contract owner, and load it's current seq + sourceAccount, err := itest.Client().AccountDetail(horizonclient.AccountRequest{ + AccountID: itest.Master().Address(), + }) + require.NoError(t, err) + + installContractOp := assembleInstallContractCodeOp(t, master.Address(), "soroban_sac_test.wasm") + preFlightOp, minFee := itest.PreflightHostFunctions(&sourceAccount, *installContractOp) + txParams := integration.GetBaseTransactionParamsWithFee(&sourceAccount, minFee+txnbuild.MinBaseFee, &preFlightOp) + _, err = itest.AsyncSubmitTransaction(master, txParams) + assert.EqualError( + t, err, + "horizon error: \"Transaction Malformed\" - check horizon.Error.Problem for more information", + ) +} + func TestAsyncTxSub_GetOpenAPISpecResponse(t *testing.T) { itest := integration.NewTest(t, integration.Config{}) res, err := http.Get(itest.AsyncTxSubOpenAPISpecURL())