Skip to content

Commit

Permalink
Add async txsub endpoint in Horizon (#989)
Browse files Browse the repository at this point in the history
* Add async txsub endpoint

* Update src/horizon/server.ts

Co-authored-by: George <[email protected]>

* Update src/horizon/server.ts

Co-authored-by: George <[email protected]>

* Update test/unit/server_async_transaction.test.js

Co-authored-by: George <[email protected]>

* Update test/unit/server_async_transaction.test.js

Co-authored-by: George <[email protected]>

* Update test/unit/server_async_transaction.test.js

Co-authored-by: George <[email protected]>

* Directly compare with response object

* Add docstring

* Update test/unit/server_async_transaction.test.js

Co-authored-by: George <[email protected]>

* Add endpoint back after main merge

---------

Co-authored-by: George <[email protected]>
  • Loading branch information
aditya1702 and Shaptic authored Jun 26, 2024
1 parent b8430dd commit d0302f9
Show file tree
Hide file tree
Showing 3 changed files with 165 additions and 0 deletions.
6 changes: 6 additions & 0 deletions src/horizon/horizon_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ export namespace HorizonApi {
paging_token: string;
}

export interface SubmitAsyncTransactionResponse {
hash: string;
tx_status: string;
error_result_xdr: string;
}

export interface FeeBumpTransactionResponse {
hash: string;
signatures: string[];
Expand Down
53 changes: 53 additions & 0 deletions src/horizon/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,59 @@ export class Server {
});
}

/**
* Submits an 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.
*
* By default, this function calls {@link Server#checkMemoRequired}, you can
* skip this check by setting the option `skipMemoRequiredCheck` to `true`.
*
* @see [Submit
* Async Transaction](https://developers.stellar.org/docs/data/horizon/api-reference/resources/submit-async-transaction)
* @param {Transaction|FeeBumpTransaction} transaction - The transaction to submit.
* @param {object} [opts] Options object
* @param {boolean} [opts.skipMemoRequiredCheck] - Allow skipping memo
* required check, default: `false`. See
* [SEP0029](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0029.md).
* @returns {Promise} Promise that resolves or rejects with response from
* horizon.
*/
public async submitAsyncTransaction(
transaction: Transaction | FeeBumpTransaction,
opts: Server.SubmitTransactionOptions = { skipMemoRequiredCheck: false }
): Promise<HorizonApi.SubmitAsyncTransactionResponse> {
// only check for memo required if skipMemoRequiredCheck is false and the transaction doesn't include a memo.
if (!opts.skipMemoRequiredCheck) {
await this.checkMemoRequired(transaction);
}

const tx = encodeURIComponent(
transaction
.toEnvelope()
.toXDR()
.toString("base64"),
);

return AxiosClient.post(
URI(this.serverURL as any)
.segment("transactions_async")
.toString(),
`tx=${tx}`,
).then((response) => response.data
).catch((response) => {
if (response instanceof Error) {
return Promise.reject(response);
}
return Promise.reject(
new BadResponseError(
`Transaction submission failed. Server responded: ${response.status} ${response.statusText}`,
response.data,
),
);
});
}

/**
* @returns {AccountCallBuilder} New {@link AccountCallBuilder} object configured by a current Horizon server configuration.
*/
Expand Down
106 changes: 106 additions & 0 deletions test/unit/server_async_transaction.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
const { Horizon } = StellarSdk;

describe("server.js async transaction submission tests", function () {
let keypair = StellarSdk.Keypair.random();
let account = new StellarSdk.Account(keypair.publicKey(), "56199647068161");

beforeEach(function () {
this.server = new Horizon.Server("https://horizon-live.stellar.org:1337");
this.axiosMock = sinon.mock(Horizon.AxiosClient);
let transaction = new StellarSdk.TransactionBuilder(account, {
fee: StellarSdk.BASE_FEE,
networkPassphrase: StellarSdk.Networks.TESTNET,
})
.addOperation(
StellarSdk.Operation.payment({
destination:
"GASOCNHNNLYFNMDJYQ3XFMI7BYHIOCFW3GJEOWRPEGK2TDPGTG2E5EDW",
asset: StellarSdk.Asset.native(),
amount: "100.50",
}),
)
.setTimeout(StellarSdk.TimeoutInfinite)
.build();
transaction.sign(keypair);

this.transaction = transaction;
this.blob = encodeURIComponent(
transaction.toEnvelope().toXDR().toString("base64"),
);
});

afterEach(function () {
this.axiosMock.verify();
this.axiosMock.restore();
});

it("sends an async transaction", function (done) {
this.axiosMock
.expects("post")
.withArgs(
"https://horizon-live.stellar.org:1337/transactions_async",
`tx=${this.blob}`,
)
.returns(Promise.resolve({ data: {} }));

this.server
.submitAsyncTransaction(this.transaction, { skipMemoRequiredCheck: true })
.then(() => done())
.catch((err) => done(err));
});
it("sends an async transaction and gets a PENDING response", function (done) {
const response = {
tx_status: "PENDING",
hash: "db2c69a07be57eb5baefbfbb72b95c7c20d2c4d6f2a0e84e7c27dd0359055a2f",
};

this.axiosMock
.expects("post")
.withArgs(
"https://horizon-live.stellar.org:1337/transactions_async",
`tx=${this.blob}`,
)
.returns(Promise.resolve({ data: response }));

this.server
.submitAsyncTransaction(this.transaction, { skipMemoRequiredCheck: true })
.then(function (res) {
expect(res).to.equal(response)
done();
})
.catch(function (err) {
done(err);
});
});
it("sends an async transaction and gets a Problem response", function (done) {
const response = {
type: "transaction_submission_exception",
title: "Transaction Submission Exception",
status: 500,
detail: "Received exception from stellar-core." +
"The `extras.error` field on this response contains further " +
"details. Descriptions of each code can be found at: " +
"https://developers.stellar.org/api/errors/http-status-codes/horizon-specific/transaction-submission-async/transaction_submission_exception",
extras: {
envelope_xdr: "AAAAAIUAEW3jQt3+fbT6nCASA1/8RWdp9fJ2woxqPHZPQUH/AAAAZAEH/OgAAAAgAAAAAQAAAAAAAAAAAAAAAFyIDD0AAAAAAAAAAQAAAAAAAAADAAAAAAAAAAFCQVQAAAAAAEZK09vHmzOmEMoVWYtbbZcKv3ZOoo06ckzbhyDIFKfhAAAAAAExLQAAAAABAAAAAgAAAAAAAAAAAAAAAAAAAAFPQUH/AAAAQHk3Igj+JXqggsJBFl4mrzgACqxWpx87psxu5UHnSskbwRjHZz89NycCZmJL4gN5WN7twm+wK371K9XcRNDiBwQ=",
error: "There was an exception when submitting this transaction."
}
};

this.axiosMock
.expects("post")
.withArgs(
"https://horizon-live.stellar.org:1337/transactions_async",
`tx=${this.blob}`,
)
.returns(Promise.resolve({ data: response }));

this.server
.submitAsyncTransaction(this.transaction, { skipMemoRequiredCheck: true })
.then(function (res) {
expect(res).to.equal(response)
done();
})
.catch((err) => done(err));
});
});

0 comments on commit d0302f9

Please sign in to comment.