diff --git a/src/main/java/org/stellar/sdk/responses/SubmitTransactionResponse.java b/src/main/java/org/stellar/sdk/responses/SubmitTransactionResponse.java index 0aebc8c3f..0658c9c31 100644 --- a/src/main/java/org/stellar/sdk/responses/SubmitTransactionResponse.java +++ b/src/main/java/org/stellar/sdk/responses/SubmitTransactionResponse.java @@ -2,8 +2,14 @@ import com.google.gson.annotations.SerializedName; +import org.apache.commons.codec.binary.Base64; import org.stellar.sdk.Server; +import org.stellar.sdk.xdr.OperationType; +import org.stellar.sdk.xdr.TransactionResult; +import org.stellar.sdk.xdr.XdrDataInputStream; +import java.io.ByteArrayInputStream; +import java.io.IOException; import java.util.ArrayList; /** @@ -15,13 +21,19 @@ public class SubmitTransactionResponse extends Response { private final String hash; @SerializedName("ledger") private final Long ledger; + @SerializedName("envelope_xdr") + private final String envelopeXdr; + @SerializedName("result_xdr") + private final String resultXdr; @SerializedName("extras") private final Extras extras; - SubmitTransactionResponse(Extras extras, Long ledger, String hash) { + SubmitTransactionResponse(Extras extras, Long ledger, String hash, String envelopeXdr, String resultXdr) { this.extras = extras; this.ledger = ledger; this.hash = hash; + this.envelopeXdr = envelopeXdr; + this.resultXdr = resultXdr; } public boolean isSuccess() { @@ -36,6 +48,56 @@ public Long getLedger() { return ledger; } + public String getEnvelopeXdr() { + if (this.isSuccess()) { + return this.envelopeXdr; + } else { + return this.getExtras().getEnvelopeXdr(); + } + } + + public String getResultXdr() { + if (this.isSuccess()) { + return this.resultXdr; + } else { + return this.getExtras().getResultXdr(); + } + } + + /** + * Helper method that returns Offer ID for ManageOffer from TransactionResult Xdr. + * This is helpful when you need ID of an offer to update it later. + * @param position Position of ManageOffer operation. If ManageOffer is second operation in this transaction this should be equal 1. + * @return Offer ID or null when operation at position is not a ManageOffer operation or error has occurred. + */ + public Long getOfferIdFromResult(int position) { + if (!this.isSuccess()) { + return null; + } + + Base64 base64Codec = new Base64(); + byte[] bytes = base64Codec.decode(this.getResultXdr()); + ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes); + XdrDataInputStream xdrInputStream = new XdrDataInputStream(inputStream); + TransactionResult result; + + try { + result = TransactionResult.decode(xdrInputStream); + } catch (IOException e) { + return null; + } + + if (result.getResult().getResults()[position] == null) { + return null; + } + + if (result.getResult().getResults()[position].getTr().getDiscriminant() != OperationType.MANAGE_OFFER) { + return null; + } + + return result.getResult().getResults()[0].getTr().getManageOfferResult().getSuccess().getOffer().getOffer().getOfferID().getUint64(); + } + /** * Additional information returned by a server. This will be null if transaction succeeded. */ diff --git a/src/test/java/org/stellar/sdk/responses/SubmitTransactionResponseTest.java b/src/test/java/org/stellar/sdk/responses/SubmitTransactionResponseTest.java new file mode 100644 index 000000000..d4f9bd194 --- /dev/null +++ b/src/test/java/org/stellar/sdk/responses/SubmitTransactionResponseTest.java @@ -0,0 +1,83 @@ +package org.stellar.sdk.responses; + +import junit.framework.TestCase; + +import org.junit.Test; + +public class SubmitTransactionResponseTest extends TestCase { + @Test + public void testDeserializeTransactionFailureResponse() { + String json = "{\n" + + " \"type\": \"https://stellar.org/horizon-errors/transaction_failed\",\n" + + " \"title\": \"Transaction Failed\",\n" + + " \"status\": 400,\n" + + " \"detail\": \"The transaction failed when submitted to the stellar network. The `extras.result_codes` field on this response contains further details. Descriptions of each code can be found at: https://www.stellar.org/developers/learn/concepts/list-of-operations.html\",\n" + + " \"instance\": \"horizon-testnet-001.prd.stellar001.internal.stellar-ops.com/4elYz2fHhC-528285\",\n" + + " \"extras\": {\n" + + " \"envelope_xdr\": \"AAAAAKpmDL6Z4hvZmkTBkYpHftan4ogzTaO4XTB7joLgQnYYAAAAZAAAAAAABeoyAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAABAAAAAD3sEVVGZGi/NoC3ta/8f/YZKMzyi9ZJpOi0H47x7IqYAAAAAAAAAAAF9eEAAAAAAAAAAAA=\",\n" + + " \"result_codes\": {\n" + + " \"transaction\": \"tx_no_source_account\"\n" + + " },\n" + + " \"result_xdr\": \"AAAAAAAAAAD////4AAAAAA==\"\n" + + " }\n" + + "}"; + + SubmitTransactionResponse submitTransactionResponse = GsonSingleton.getInstance().fromJson(json, SubmitTransactionResponse.class); + assertEquals(submitTransactionResponse.isSuccess(), false); + assertEquals(submitTransactionResponse.getEnvelopeXdr(), "AAAAAKpmDL6Z4hvZmkTBkYpHftan4ogzTaO4XTB7joLgQnYYAAAAZAAAAAAABeoyAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAABAAAAAD3sEVVGZGi/NoC3ta/8f/YZKMzyi9ZJpOi0H47x7IqYAAAAAAAAAAAF9eEAAAAAAAAAAAA="); + assertEquals(submitTransactionResponse.getResultXdr(), "AAAAAAAAAAD////4AAAAAA=="); + assertEquals(submitTransactionResponse.getExtras().getResultCodes().getTransactionResultCode(), "tx_no_source_account"); + } + + @Test + public void testDeserializeOperationFailureResponse() { + String json = "{\n" + + " \"type\": \"https://stellar.org/horizon-errors/transaction_failed\",\n" + + " \"title\": \"Transaction Failed\",\n" + + " \"status\": 400,\n" + + " \"detail\": \"The transaction failed when submitted to the stellar network. The `extras.result_codes` field on this response contains further details. Descriptions of each code can be found at: https://www.stellar.org/developers/learn/concepts/list-of-operations.html\",\n" + + " \"instance\": \"horizon-testnet-001.prd.stellar001.internal.stellar-ops.com/4elYz2fHhC-528366\",\n" + + " \"extras\": {\n" + + " \"envelope_xdr\": \"AAAAAF2O0axA67+p2jMunG6G188kDSHIvqQ13d9l29YCSA/uAAAAZAAvvc0AAAABAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAABAAAAAD3sEVVGZGi/NoC3ta/8f/YZKMzyi9ZJpOi0H47x7IqYAAAAAAAAAAAF9eEAAAAAAAAAAAECSA/uAAAAQFuZVAjftHa+JZes1VxSk8naOfjjAz9V86mY1AZf8Ik6PtTsBpDsCfG57EYsq4jWyZcT+vhXyWsw5evF1ELqMw4=\",\n" + + " \"result_codes\": {\n" + + " \"transaction\": \"tx_failed\",\n" + + " \"operations\": [\n" + + " \"op_no_destination\"\n" + + " ]\n" + + " },\n" + + " \"result_xdr\": \"AAAAAAAAAGT/////AAAAAQAAAAAAAAAB////+wAAAAA=\"\n" + + " }\n" + + "}"; + + SubmitTransactionResponse submitTransactionResponse = GsonSingleton.getInstance().fromJson(json, SubmitTransactionResponse.class); + assertEquals(submitTransactionResponse.isSuccess(), false); + assertEquals(submitTransactionResponse.getEnvelopeXdr(), "AAAAAF2O0axA67+p2jMunG6G188kDSHIvqQ13d9l29YCSA/uAAAAZAAvvc0AAAABAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAABAAAAAD3sEVVGZGi/NoC3ta/8f/YZKMzyi9ZJpOi0H47x7IqYAAAAAAAAAAAF9eEAAAAAAAAAAAECSA/uAAAAQFuZVAjftHa+JZes1VxSk8naOfjjAz9V86mY1AZf8Ik6PtTsBpDsCfG57EYsq4jWyZcT+vhXyWsw5evF1ELqMw4="); + assertEquals(submitTransactionResponse.getResultXdr(), "AAAAAAAAAGT/////AAAAAQAAAAAAAAAB////+wAAAAA="); + assertEquals(submitTransactionResponse.getExtras().getResultCodes().getTransactionResultCode(), "tx_failed"); + assertEquals(submitTransactionResponse.getExtras().getResultCodes().getOperationsResultCodes().get(0), "op_no_destination"); + } + + @Test + public void testDeserializeSuccessResponse() { + String json = "{\n" + + " \"_links\": {\n" + + " \"transaction\": {\n" + + " \"href\": \"https://horizon-testnet.stellar.org/transactions/ee14b93fcd31d4cfe835b941a0a8744e23a6677097db1fafe0552d8657bed940\"\n" + + " }\n" + + " },\n" + + " \"hash\": \"ee14b93fcd31d4cfe835b941a0a8744e23a6677097db1fafe0552d8657bed940\",\n" + + " \"ledger\": 3128812,\n" + + " \"envelope_xdr\": \"AAAAADSMMRmQGDH6EJzkgi/7PoKhphMHyNGQgDp2tlS/dhGXAAAAZAAT3TUAAAAwAAAAAAAAAAAAAAABAAAAAAAAAAMAAAABSU5SAAAAAAA0jDEZkBgx+hCc5IIv+z6CoaYTB8jRkIA6drZUv3YRlwAAAAFVU0QAAAAAADSMMRmQGDH6EJzkgi/7PoKhphMHyNGQgDp2tlS/dhGXAAAAAAX14QAAAAAKAAAAAQAAAAAAAAAAAAAAAAAAAAG/dhGXAAAAQLuStfImg0OeeGAQmvLkJSZ1MPSkCzCYNbGqX5oYNuuOqZ5SmWhEsC7uOD9ha4V7KengiwNlc0oMNqBVo22S7gk=\",\n" + + " \"result_xdr\": \"AAAAAAAAAGQAAAAAAAAAAQAAAAAAAAADAAAAAAAAAAAAAAAAAAAAADSMMRmQGDH6EJzkgi/7PoKhphMHyNGQgDp2tlS/dhGXAAAAAAAAAPEAAAABSU5SAAAAAAA0jDEZkBgx+hCc5IIv+z6CoaYTB8jRkIA6drZUv3YRlwAAAAFVU0QAAAAAADSMMRmQGDH6EJzkgi/7PoKhphMHyNGQgDp2tlS/dhGXAAAAAAX14QAAAAAKAAAAAQAAAAAAAAAAAAAAAA==\",\n" + + " \"result_meta_xdr\": \"AAAAAAAAAAEAAAACAAAAAAAvoHwAAAACAAAAADSMMRmQGDH6EJzkgi/7PoKhphMHyNGQgDp2tlS/dhGXAAAAAAAAAPEAAAABSU5SAAAAAAA0jDEZkBgx+hCc5IIv+z6CoaYTB8jRkIA6drZUv3YRlwAAAAFVU0QAAAAAADSMMRmQGDH6EJzkgi/7PoKhphMHyNGQgDp2tlS/dhGXAAAAAAX14QAAAAAKAAAAAQAAAAAAAAAAAAAAAAAAAAEAL6B8AAAAAAAAAAA0jDEZkBgx+hCc5IIv+z6CoaYTB8jRkIA6drZUv3YRlwAAABZ9zvNAABPdNQAAADAAAAAEAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA==\"\n" + + "}"; + + SubmitTransactionResponse submitTransactionResponse = GsonSingleton.getInstance().fromJson(json, SubmitTransactionResponse.class); + assertEquals(submitTransactionResponse.isSuccess(), true); + assertEquals(submitTransactionResponse.getHash(), "ee14b93fcd31d4cfe835b941a0a8744e23a6677097db1fafe0552d8657bed940"); + assertEquals(submitTransactionResponse.getLedger(), new Long(3128812)); + assertEquals(submitTransactionResponse.getEnvelopeXdr(), "AAAAADSMMRmQGDH6EJzkgi/7PoKhphMHyNGQgDp2tlS/dhGXAAAAZAAT3TUAAAAwAAAAAAAAAAAAAAABAAAAAAAAAAMAAAABSU5SAAAAAAA0jDEZkBgx+hCc5IIv+z6CoaYTB8jRkIA6drZUv3YRlwAAAAFVU0QAAAAAADSMMRmQGDH6EJzkgi/7PoKhphMHyNGQgDp2tlS/dhGXAAAAAAX14QAAAAAKAAAAAQAAAAAAAAAAAAAAAAAAAAG/dhGXAAAAQLuStfImg0OeeGAQmvLkJSZ1MPSkCzCYNbGqX5oYNuuOqZ5SmWhEsC7uOD9ha4V7KengiwNlc0oMNqBVo22S7gk="); + assertEquals(submitTransactionResponse.getResultXdr(), "AAAAAAAAAGQAAAAAAAAAAQAAAAAAAAADAAAAAAAAAAAAAAAAAAAAADSMMRmQGDH6EJzkgi/7PoKhphMHyNGQgDp2tlS/dhGXAAAAAAAAAPEAAAABSU5SAAAAAAA0jDEZkBgx+hCc5IIv+z6CoaYTB8jRkIA6drZUv3YRlwAAAAFVU0QAAAAAADSMMRmQGDH6EJzkgi/7PoKhphMHyNGQgDp2tlS/dhGXAAAAAAX14QAAAAAKAAAAAQAAAAAAAAAAAAAAAA=="); + assertEquals(submitTransactionResponse.getOfferIdFromResult(0), new Long(241)); + } +}