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
222 changes: 128 additions & 94 deletions src/horizon/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,18 @@ const STROOPS_IN_LUMEN = 10000000;
// SEP 29 uses this value to define transaction memo requirements for incoming payments.
const ACCOUNT_REQUIRES_MEMO = "MQ==";

/**
*
* @param amt
*/
function _getAmountInLumens(amt: BigNumber) {
return new BigNumber(amt).div(STROOPS_IN_LUMEN).toString();
}

/**
* Server handles the network connection to a [Horizon](https://developers.stellar.org/api/introduction/)
* instance and exposes an interface for requests to that instance.
* @constructor
* @class
* @param {string} serverURL Horizon Server URL (ex. `https://horizon-testnet.stellar.org`).
* @param {object} [opts] Options object
* @param {boolean} [opts.allowHttp] - Allow connecting to http servers, default: `false`. This must be set to false in production deployments! You can also use {@link Config} class to set this globally.
Expand Down Expand Up @@ -131,8 +135,10 @@ export class Server {
* // earlier does the trick!
* .build();
* ```
* @argument {number} seconds Number of seconds past the current time to wait.
* @argument {bool} [_isRetry=false] True if this is a retry. Only set this internally!
* @param seconds
* @param {number} seconds Number of seconds past the current time to wait.
* @param _isRetry
* @param {bool} [_isRetry] True if this is a retry. Only set this internally!
aditya1702 marked this conversation as resolved.
Show resolved Hide resolved
* This is to avoid a scenario where Horizon is horking up the wrong date.
* @returns {Promise<Timebounds>} Promise that resolves a `timebounds` object
* (with the shape `{ minTime: 0, maxTime: N }`) that you can set the `timebounds` option to.
Expand Down Expand Up @@ -162,7 +168,7 @@ export class Server {
// otherwise, retry (by calling the root endpoint)
// toString automatically adds the trailing slash
await AxiosClient.get(URI(this.serverURL as any).toString());
return await this.fetchTimebounds(seconds, true);
return this.fetchTimebounds(seconds, true);
}

/**
Expand Down Expand Up @@ -203,87 +209,86 @@ export class Server {
* Ex:
* ```javascript
* const res = {
* ...response,
* offerResults: [
* {
* // Exact ordered list of offers that executed, with the exception
* // that the last one may not have executed entirely.
* offersClaimed: [
* sellerId: String,
* offerId: String,
* assetSold: {
* type: 'native|credit_alphanum4|credit_alphanum12',
*
* // these are only present if the asset is not native
* assetCode: String,
* issuer: String,
* },
*
* // same shape as assetSold
* assetBought: {}
* ],
*
* // What effect your manageOffer op had
* effect: "manageOfferCreated|manageOfferUpdated|manageOfferDeleted",
*
* // Whether your offer immediately got matched and filled
* wasImmediatelyFilled: Boolean,
*
* // Whether your offer immediately got deleted, if for example the order was too small
* wasImmediatelyDeleted: Boolean,
*
* // Whether the offer was partially, but not completely, filled
* wasPartiallyFilled: Boolean,
*
* // The full requested amount of the offer is open for matching
* isFullyOpen: Boolean,
*
* // The total amount of tokens bought / sold during transaction execution
* amountBought: Number,
* amountSold: Number,
*
* // if the offer was created, updated, or partially filled, this is
* // the outstanding offer
* currentOffer: {
* offerId: String,
* amount: String,
* price: {
* n: String,
* d: String,
* },
*
* selling: {
* type: 'native|credit_alphanum4|credit_alphanum12',
*
* // these are only present if the asset is not native
* assetCode: String,
* issuer: String,
* },
*
* // same as `selling`
* buying: {},
* },
*
* // the index of this particular operation in the op stack
* operationIndex: Number
* }
* ]
* ...response,
* offerResults: [
* {
* // Exact ordered list of offers that executed, with the exception
* // that the last one may not have executed entirely.
* offersClaimed: [
* sellerId: String,
* offerId: String,
* assetSold: {
* type: 'native|credit_alphanum4|credit_alphanum12',
*
* // these are only present if the asset is not native
* assetCode: String,
* issuer: String,
* },
*
* // same shape as assetSold
* assetBought: {}
* ],
*
* // What effect your manageOffer op had
* effect: "manageOfferCreated|manageOfferUpdated|manageOfferDeleted",
*
* // Whether your offer immediately got matched and filled
* wasImmediatelyFilled: Boolean,
*
* // Whether your offer immediately got deleted, if for example the order was too small
* wasImmediatelyDeleted: Boolean,
*
* // Whether the offer was partially, but not completely, filled
* wasPartiallyFilled: Boolean,
*
* // The full requested amount of the offer is open for matching
* isFullyOpen: Boolean,
*
* // The total amount of tokens bought / sold during transaction execution
* amountBought: Number,
* amountSold: Number,
*
* // if the offer was created, updated, or partially filled, this is
* // the outstanding offer
* currentOffer: {
* offerId: String,
* amount: String,
* price: {
* n: String,
* d: String,
* },
*
* selling: {
* type: 'native|credit_alphanum4|credit_alphanum12',
*
* // these are only present if the asset is not native
* assetCode: String,
* issuer: String,
* },
*
* // same as `selling`
* buying: {},
* },
*
* // the index of this particular operation in the op stack
* operationIndex: Number
* }
* ]
* }
* ```
*
* For example, you'll want to examine `offerResults` to add affordances like
* these to your app:
* * If `wasImmediatelyFilled` is true, then no offer was created. So if you
* normally watch the `Server.offers` endpoint for offer updates, you
* instead need to check `Server.trades` to find the result of this filled
* offer.
* * If `wasImmediatelyDeleted` is true, then the offer you submitted was
* deleted without reaching the orderbook or being matched (possibly because
* your amounts were rounded down to zero). So treat the just-submitted
* offer request as if it never happened.
* * If `wasPartiallyFilled` is true, you can tell the user that
* `amountBought` or `amountSold` have already been transferred.
*
* If `wasImmediatelyFilled` is true, then no offer was created. So if you
* normally watch the `Server.offers` endpoint for offer updates, you
* instead need to check `Server.trades` to find the result of this filled
* offer.
* If `wasImmediatelyDeleted` is true, then the offer you submitted was
* deleted without reaching the orderbook or being matched (possibly because
* your amounts were rounded down to zero). So treat the just-submitted
* offer request as if it never happened.
* If `wasPartiallyFilled` is true, you can tell the user that
* `amountBought` or `amountSold` have already been transferred.
* @see [Post
* Transaction](https://developers.stellar.org/api/resources/transactions/post/)
* @param {Transaction|FeeBumpTransaction} transaction - The transaction to submit.
Expand Down Expand Up @@ -377,8 +382,8 @@ export class Server {
// However, you can never be too careful.
default:
throw new Error(
"Invalid offer result type: " +
offerClaimedAtom.switch(),
`Invalid offer result type: ${
offerClaimedAtom.switch()}`,
);
}

Expand Down Expand Up @@ -488,9 +493,7 @@ export class Server {
.filter((result: any) => !!result);
}

return Object.assign({}, response.data, {
offerResults: hasManageOffer ? offerResults : undefined,
});
return { ...response.data, offerResults: hasManageOffer ? offerResults : undefined,};
})
.catch((response) => {
if (response instanceof Error) {
Expand All @@ -505,6 +508,42 @@ export class Server {
});
}

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) => {
return response.data
}).catch((response) => {
aditya1702 marked this conversation as resolved.
Show resolved Hide resolved
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 Expand Up @@ -595,9 +634,9 @@ export class Server {
*
* A strict receive path search is specified using:
*
* * The destination address.
* * The source address or source assets.
* * The asset and amount that the destination account should receive.
* The destination address.
* The source address or source assets.
* The asset and amount that the destination account should receive.
*
* As part of the search, horizon will load a list of assets available to the
* source address and will find any payment paths from those source assets to
Expand All @@ -607,7 +646,6 @@ export class Server {
*
* If a list of assets is passed as the source, horizon will find any payment
* paths from those source assets to the desired destination asset.
*
* @param {string|Asset[]} source The sender's account ID or a list of assets. Any returned path will use a source that the sender can hold.
* @param {Asset} destinationAsset The destination asset.
* @param {string} destinationAmount The amount, denominated in the destination asset, that any returned path should be able to satisfy.
Expand Down Expand Up @@ -635,7 +673,6 @@ export class Server {
*
* The asset and amount that is being sent.
* The destination account or the destination assets.
*
* @param {Asset} sourceAsset The asset to be sent.
* @param {string} sourceAmount The amount, denominated in the source asset, that any returned path should be able to satisfy.
* @param {string|Asset[]} destination The destination account or the destination assets.
Expand Down Expand Up @@ -692,9 +729,7 @@ export class Server {
/**
* Fetches an account's most current state in the ledger, then creates and
* returns an {@link AccountResponse} object.
*
* @param {string} accountId - The account to load.
*
* @returns {Promise} Returns a promise to the {@link AccountResponse} object
* with populated sequence number.
*/
Expand Down Expand Up @@ -746,7 +781,6 @@ export class Server {
*
* Each account is checked sequentially instead of loading multiple accounts
* at the same time from Horizon.
*
* @see https://stellar.org/protocol/sep-29
* @param {Transaction} transaction - The transaction to check.
* @returns {Promise<void, Error>} - If any of the destination account
Expand Down Expand Up @@ -778,7 +812,7 @@ export class Server {
default:
continue;
}
const destination = operation.destination;
const {destination} = operation;
if (destinations.has(destination)) {
continue;
}
Expand Down
Loading
Loading