diff --git a/src/rpc/index.ts b/src/rpc/index.ts index 8564852bf..3e9271097 100644 --- a/src/rpc/index.ts +++ b/src/rpc/index.ts @@ -7,7 +7,12 @@ export * from "./api"; // soroban-client classes to expose -export { RpcServer as Server, Durability } from "./server"; +export { + RpcServer as Server, + BasicSleepStrategy, + LinearSleepStrategy, + Durability +} from "./server"; export { default as AxiosClient } from "./axios"; export { parseRawSimulation, parseRawEvents } from "./parsers"; export * from "./transaction"; diff --git a/src/rpc/server.ts b/src/rpc/server.ts index 982154b9c..546d707f8 100644 --- a/src/rpc/server.ts +++ b/src/rpc/server.ts @@ -30,6 +30,7 @@ import { parseRawTransactions, parseTransactionInfo, } from './parsers'; +import { Utils } from '../utils'; /** * Default transaction submission timeout for RPC requests, in milliseconds @@ -84,6 +85,11 @@ export namespace RpcServer { limit?: number; } + export interface PollingOptions { + attempts?: number; + sleepStrategy?: SleepStrategy; + } + export interface ResourceLeeway { cpuInstructions: number; } @@ -95,6 +101,22 @@ export namespace RpcServer { } } +const DEFAULT_GET_TRANSACTION_TIMEOUT: number = 30; + +/// A strategy that will sleep 1 second each time +export const BasicSleepStrategy: SleepStrategy = +(_iter: number) => 1000; + +/// A strategy that will sleep 1 second longer on each attempt +export const LinearSleepStrategy: SleepStrategy = + (iter: number) => 1000 * iter; + +/** + * A function that returns the number of *milliseconds* to sleep + * on a given `iter`ation. + */ +export type SleepStrategy = (iter: number) => number; + function findCreatedAccountSequenceInTransactionMeta( meta: xdr.TransactionMeta ): string { @@ -453,6 +475,55 @@ export class RpcServer { ); } + + /** + * Poll for a particular transaction with certain parameters. + * + * After submitting a transaction, clients can use this to poll for + * transaction completion and return a definitive state of success or failure. + * + * @param {string} hash the transaction you're polling for + * @param {number} [opts.attempts] (optional) the number of attempts to make + * before returning the last-seen status. By default or on invalid inputs, + * try 5 times. + * @param {SleepStrategy} [opts.sleepStrategy] (optional) the amount of time + * to wait for between each attempt. By default, sleep for 1 second between + * each attempt. + * + * @return {Promise} the response after a "found" + * response (which may be success or failure) or the last response obtained + * after polling the maximum number of specified attempts. + * + * @example + * const h = "c4515e3bdc0897f21cc5dbec8c82cf0a936d4741cb74a8e158eb51b9fb00411a"; + * const txStatus = await server.pollTransaction(h, { + * attempts: 100, // I'm a maniac + * sleepStrategy: rpc.LinearSleepStrategy + * }); // this will take 5,050 seconds to complete + */ + public async pollTransaction( + hash: string, + opts?: RpcServer.PollingOptions + ): Promise { + let maxAttempts: number = ( + (opts?.attempts ?? 0) < 1 + ? DEFAULT_GET_TRANSACTION_TIMEOUT + : (opts?.attempts ?? DEFAULT_GET_TRANSACTION_TIMEOUT) + ); // "positive and defined user value or default" + + let foundInfo: Api.GetTransactionResponse; + for (let attempt = 1; attempt < maxAttempts; attempt++) { + foundInfo = await this.getTransaction(hash); + if (foundInfo.status !== Api.GetTransactionStatus.NOT_FOUND) { + return foundInfo; + } + + await Utils.sleep((opts?.sleepStrategy ?? BasicSleepStrategy)(attempt)); + } + + return foundInfo!; + } + /** * Fetch the details of a submitted transaction. * @@ -460,10 +531,11 @@ export class RpcServer { * transaction has completed. * * @param {string} hash Hex-encoded hash of the transaction to check - * @returns {Promise} The status, - * result, and other details about the transaction + * @returns {Promise} The status, result, and + * other details about the transaction * - * @see {@link https://developers.stellar.org/docs/data/rpc/api-reference/methods/getTransaction | getTransaction docs} + * @see + * {@link https://developers.stellar.org/docs/data/rpc/api-reference/methods/getTransaction | getTransaction docs} * * @example * const transactionHash = "c4515e3bdc0897f21cc5dbec8c82cf0a936d4741cb74a8e158eb51b9fb00411a"; @@ -480,8 +552,8 @@ export class RpcServer { ): Promise { return this._getTransaction(hash).then((raw) => { const foundInfo: Omit< - Api.GetSuccessfulTransactionResponse, - keyof Api.GetMissingTransactionResponse + Api.GetSuccessfulTransactionResponse, + keyof Api.GetMissingTransactionResponse > = {} as any; if (raw.status !== Api.GetTransactionStatus.NOT_FOUND) { diff --git a/src/utils.ts b/src/utils.ts index 9a2a46b3d..00451a687 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -32,4 +32,8 @@ export class Utils { now <= Number.parseInt(maxTime, 10) + gracePeriod ); } + + static sleep(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); + } }