Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add async txsub endpoint in Horizon #989

Merged
merged 11 commits into from
Jun 26, 2024
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(
aditya1702 marked this conversation as resolved.
Show resolved Hide resolved
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));
});
});
Loading