Repository: gelatodigital/smartwallet
Pull request #34: feat: handler to get tx hash on task submission
Author: login: mkykadir
Created: 2025-05-06T12:25:51Z | Merged: 2025-05-08T16:46:55Z



Diff:
diff --git a/.changeset/fluffy-ghosts-occur.md b/.changeset/fluffy-ghosts-occur.md
new file mode 100644
index 00000000..5afb564d
--- /dev/null
+++ b/.changeset/fluffy-ghosts-occur.md
@@ -0,0 +1,5 @@
+---
+"@gelatonetwork/smartwallet": patch
+---
+
+feat: handler for tx hash on task submission
diff --git a/examples/react-vite/src/providers.tsx b/examples/react-vite/src/providers.tsx
index 53a3f50f..8ceab0db 100644
--- a/examples/react-vite/src/providers.tsx
+++ b/examples/react-vite/src/providers.tsx
@@ -11,6 +11,11 @@ import { http, useAccount } from "wagmi";
 
 import { sepolia } from "viem/chains";
 
+enum TransactionStatus {
+  Submitted = "submitted",
+  Executed = "executed"
+}
+
 const WalletInfoComponent = () => {
   const {
     gelato: { client },
@@ -22,7 +27,9 @@ const WalletInfoComponent = () => {
   const [erc20TokenAddress, setErc20TokenAddress] = useState<`0x${string}`>(
     "0x7b79995e5f793A07Bc00c21412e50Ecae098E7f9"
   );
+
   const [transactionHash, setTransactionHash] = useState<string | null>(null);
+  const [transactionStatus, setTransactionStatus] = useState<TransactionStatus | null>(null);
   const [isLoading, setIsLoading] = useState<boolean>(false);
   const { address: walletAddress } = useAccount();
   const sponsorApiKey = import.meta.env.VITE_SPONSOR_API_KEY;
@@ -50,13 +57,18 @@ const WalletInfoComponent = () => {
         ]
       });
 
+      response.on("submitted", (status: GelatoTaskStatus) => {
+        console.log("Transaction submitted:", status.transactionHash);
+        // biome-ignore lint/style/noNonNullAssertion: <explanation>
+        setTransactionHash(status.transactionHash!);
+        setTransactionStatus(TransactionStatus.Submitted);
+      });
       response.on("success", (status: GelatoTaskStatus) => {
         console.log("Transaction successful:", status.transactionHash);
+        // biome-ignore lint/style/noNonNullAssertion: <explanation>
+        setTransactionHash(status.transactionHash!);
+        setTransactionStatus(TransactionStatus.Executed);
       });
-
-      const transactionHash = await response.wait();
-
-      setTransactionHash(transactionHash);
     } catch (error) {
       console.error("Transaction failed:", error);
     } finally {
@@ -119,16 +131,19 @@ const WalletInfoComponent = () => {
                 {isLoading ? "Processing..." : "Execute Transaction"}
               </button>
               {transactionHash && (
-                <p>
-                  Transaction Hash:{" "}
-                  <a
-                    href={`https://sepolia.etherscan.io/tx/${transactionHash}`}
-                    target="_blank"
-                    rel="noopener noreferrer"
-                  >
-                    {transactionHash}
-                  </a>
-                </p>
+                <div>
+                  <p>Transaction Status: {transactionStatus}</p>
+                  <p>
+                    Transaction Hash:{" "}
+                    <a
+                      href={`https://sepolia.etherscan.io/tx/${transactionHash}`}
+                      target="_blank"
+                      rel="noopener noreferrer"
+                    >
+                      {transactionHash}
+                    </a>
+                  </p>
+                </div>
               )}
             </div>
           </div>
diff --git a/src/actions/internal/sendTransaction.ts b/src/actions/internal/sendTransaction.ts
index 4e7c36b8..3d14a32f 100644
--- a/src/actions/internal/sendTransaction.ts
+++ b/src/actions/internal/sendTransaction.ts
@@ -25,14 +25,17 @@ export async function sendTransaction<
 ): Promise<GelatoResponse> {
   switch (payment.type) {
     case "native": {
-      return smartWalletCall({
-        chainId: client.chain.id,
-        target,
-        feeToken: ethAddress,
-        data,
-        sponsorApiKey: client._internal.apiKey(),
-        authorizationList
-      });
+      return smartWalletCall(
+        {
+          chainId: client.chain.id,
+          target,
+          feeToken: ethAddress,
+          data,
+          sponsorApiKey: client._internal.apiKey(),
+          authorizationList
+        },
+        client
+      );
     }
     case "sponsored": {
       const sponsorApiKey = payment.sponsorApiKey ?? client._internal.apiKey();
@@ -41,23 +44,29 @@ export async function sendTransaction<
         throw new Error("Sponsor API key is required");
       }
 
-      return sponsoredCall({
-        chainId: client.chain.id,
-        target,
-        data,
-        sponsorApiKey,
-        authorizationList
-      });
+      return sponsoredCall(
+        {
+          chainId: client.chain.id,
+          target,
+          data,
+          sponsorApiKey,
+          authorizationList
+        },
+        client
+      );
     }
     case "erc20": {
-      return smartWalletCall({
-        chainId: client.chain.id,
-        target,
-        feeToken: payment.token,
-        data,
-        sponsorApiKey: client._internal.apiKey(),
-        authorizationList
-      });
+      return smartWalletCall(
+        {
+          chainId: client.chain.id,
+          target,
+          feeToken: payment.token,
+          data,
+          sponsorApiKey: client._internal.apiKey(),
+          authorizationList
+        },
+        client
+      );
     }
     default: {
       throw new Error("Unsupported payment type");
diff --git a/src/relay/actions/internal/waitHttp.ts b/src/relay/actions/internal/waitHttp.ts
index ad74226c..9869f0c2 100644
--- a/src/relay/actions/internal/waitHttp.ts
+++ b/src/relay/actions/internal/waitHttp.ts
@@ -7,8 +7,9 @@ import {
   InternalError,
   TaskState
 } from "../../status/types.js";
+import { isSubmitted } from "../../status/utils.js";
 
-export const waitHttp = async (taskId: string): Promise<Hash | undefined> => {
+export const waitHttp = async (taskId: string, submission = false): Promise<Hash | undefined> => {
   const taskStatus = await getTaskStatus(taskId);
 
   if (!taskStatus) {
@@ -33,4 +34,8 @@ export const waitHttp = async (taskId: string): Promise<Hash | undefined> => {
 
     throw new InternalError(taskId);
   }
+
+  if (submission && isSubmitted(taskStatus.taskState)) {
+    return taskStatus.transactionHash as Hash;
+  }
 };
diff --git a/src/relay/actions/internal/waitPolling.ts b/src/relay/actions/internal/waitPolling.ts
index aa928e1e..b5e25cf5 100644
--- a/src/relay/actions/internal/waitPolling.ts
+++ b/src/relay/actions/internal/waitPolling.ts
@@ -1,27 +1,56 @@
-import type { Hash } from "viem";
+import type { Account, Chain, Hash, Transport } from "viem";
 
 import { waitHttp } from "./waitHttp.js";
 
+import type { GelatoWalletClient } from "../../../actions/index.js";
 import { statusApiPollingInterval, statusApiPollingMaxRetries } from "../../../constants/index.js";
 import { ExecutionTimeoutError } from "../../status/types.js";
 
-export const waitPolling = async (
+export const waitPolling = async <
+  transport extends Transport = Transport,
+  chain extends Chain = Chain,
+  account extends Account = Account
+>(
   taskId: string,
+  submission: boolean,
+  client: GelatoWalletClient<transport, chain, account>,
+  submissionHash?: Hash,
   pollInterval?: number,
   maxRetries?: number
 ): Promise<Hash> => {
   const _pollInterval = pollInterval ?? statusApiPollingInterval();
   const _maxRetries = maxRetries ?? statusApiPollingMaxRetries();
 
-  for (let attempt = 0; attempt < _maxRetries; attempt++) {
-    const transactionHash = await waitHttp(taskId);
+  const httpRequest = async () => {
+    for (let attempt = 0; attempt < _maxRetries; attempt++) {
+      const transactionHash = await waitHttp(taskId, submission);
 
-    if (transactionHash) {
-      return transactionHash;
+      if (transactionHash) {
+        return { transactionHash };
+      }
+
+      // If not in a final state, wait before polling again
+      await new Promise((resolve) => setTimeout(resolve, _pollInterval));
     }
+    return undefined;
+  };
+
+  // Waiting for execution but TX hash gathered from submission event already
+  // race with provider's receipt
+  if (!submission && submissionHash) {
+    const response = await Promise.race([
+      httpRequest(),
+      client.waitForTransactionReceipt({ hash: submissionHash, pollingInterval: pollInterval })
+    ]);
 
-    // If not in a final state, wait before polling again
-    await new Promise((resolve) => setTimeout(resolve, _pollInterval));
+    if (response) {
+      return response.transactionHash;
+    }
+  } else {
+    const response = await httpRequest();
+    if (response) {
+      return response.transactionHash;
+    }
   }
 
   throw new ExecutionTimeoutError(taskId);
diff --git a/src/relay/actions/on.ts b/src/relay/actions/on.ts
index 32296e01..0558bb7b 100644
--- a/src/relay/actions/on.ts
+++ b/src/relay/actions/on.ts
@@ -1,5 +1,6 @@
 import type { GelatoTaskEvent, TransactionStatusResponse } from "../status/index.js";
 import { TaskState } from "../status/index.js";
+import { isSubmitted } from "../status/utils.js";
 import { statusApiWebSocket } from "../ws.js";
 import { type ErrorCallback, onError } from "./internal/onError.js";
 
@@ -30,6 +31,12 @@ export const on = (
       successCallback(taskStatus);
     } else if (update === "cancel" && taskStatus.taskState === TaskState.Cancelled) {
       successCallback(taskStatus);
+    } else if (
+      update === "submitted" &&
+      isSubmitted(taskStatus.taskState) &&
+      taskStatus.transactionHash
+    ) {
+      successCallback(taskStatus);
     }
   };
 
diff --git a/src/relay/actions/wait.ts b/src/relay/actions/wait.ts
index 69a04aab..2e5882c8 100644
--- a/src/relay/actions/wait.ts
+++ b/src/relay/actions/wait.ts
@@ -1,8 +1,10 @@
-import type { Hash } from "viem";
+import type { Account, Chain, Hash, Transport } from "viem";
 
 import { waitHttp } from "./internal/waitHttp.js";
 import { waitPolling } from "./internal/waitPolling.js";
 
+import type { GelatoWalletClient } from "../../actions/index.js";
+import { statusApiPollingInterval } from "../../constants/index.js";
 import {
   ExecutionCancelledError,
   ExecutionRevertedError,
@@ -11,23 +13,39 @@ import {
   TaskState,
   type TransactionStatusResponse
 } from "../status/types.js";
+import { isSubmitted } from "../status/utils.js";
 import { statusApiWebSocket } from "../ws.js";
 
-export const wait = async (
+type TaskStatusReturn = { hash: Hash; waitForReceipt?: boolean };
+
+/**
+ * Wait for a task to be executed on or submitted to chain
+ * @param taskId - The ID of the task to wait for
+ * @param submission - Whether to wait for submission to chain, when false execution will be waited
+ * @param parameters - Optional parameters to configure polling and fallback retries
+ * @returns The transaction hash of the task when task is executed on chain
+ */
+export const wait = async <
+  transport extends Transport = Transport,
+  chain extends Chain = Chain,
+  account extends Account = Account
+>(
   taskId: string,
+  client: GelatoWalletClient<transport, chain, account>,
+  submission = false,
   parameters?: {
     pollingInterval?: number;
     maxRetries?: number;
   }
 ): Promise<Hash> => {
   // Check with HTTP first
-  const transactionHash = await waitHttp(taskId);
+  const transactionHash = await waitHttp(taskId, submission);
 
   if (transactionHash) {
     return transactionHash;
   }
 
-  let resolvePromise: (value: Hash) => void;
+  let resolvePromise: (value: TaskStatusReturn) => void;
   let rejectPromise: (reason: Error) => void;
 
   const updateHandler = (taskStatus: TransactionStatusResponse) => {
@@ -46,9 +64,13 @@ export const wait = async (
       rejectPromise(new ExecutionCancelledError(taskId));
     }
 
+    if (isSubmitted(taskStatus.taskState) && taskStatus.transactionHash) {
+      resolvePromise({ hash: taskStatus.transactionHash as Hash, waitForReceipt: !submission });
+    }
+
     if (taskStatus.taskState === TaskState.ExecSuccess) {
       taskStatus.transactionHash
-        ? resolvePromise(taskStatus.transactionHash as Hash)
+        ? resolvePromise({ hash: taskStatus.transactionHash as Hash })
         : rejectPromise(new InternalError(taskId));
     }
   };
@@ -57,8 +79,11 @@ export const wait = async (
     rejectPromise(error);
   };
 
+  // Temporary TX hash in-case falling back to HTTP from WS
+  let fallbackHash: Hash | undefined;
+
   try {
-    const promise = new Promise<Hash>((resolve, reject) => {
+    const promise = new Promise<TaskStatusReturn>((resolve, reject) => {
       resolvePromise = resolve;
       rejectPromise = reject;
     });
@@ -67,7 +92,37 @@ export const wait = async (
     statusApiWebSocket.onError(errorHandler);
 
     await statusApiWebSocket.subscribe(taskId);
-    return await promise;
+
+    // Listen for task status, when submission event is received but we're waiting for the executionn
+    // `promise` will resolve with `waitForReceipt` set to true and Provider's TX receipt will race with
+    // status API to fetch inclusion as quickly as possible
+    const result = await promise;
+    while (result.waitForReceipt && !submission) {
+      fallbackHash = result.hash;
+      const promise = new Promise<TaskStatusReturn>((resolve, reject) => {
+        resolvePromise = resolve;
+        rejectPromise = reject;
+      });
+
+      const _result = await Promise.race([
+        promise,
+        client.waitForTransactionReceipt({
+          hash: result.hash,
+          pollingInterval: statusApiPollingInterval()
+        })
+      ]);
+
+      if ("waitForReceipt" in _result && _result.waitForReceipt) {
+        // Resubmission occurred, race for new hash again
+        result.hash = _result.hash;
+        result.waitForReceipt = _result.waitForReceipt;
+      } else {
+        // Either transaction receipt succeeded or we got ExecSuccess event
+        break;
+      }
+    }
+
+    return result.hash;
   } catch (error) {
     if (error instanceof GelatoTaskError) {
       throw error;
@@ -79,7 +134,14 @@ export const wait = async (
 
     // Websocket error happened fallback to HTTP polling
     console.warn("WebSocket connection failed, falling back to HTTP polling");
-    return await waitPolling(taskId, parameters?.pollingInterval, parameters?.maxRetries);
+    return await waitPolling(
+      taskId,
+      submission,
+      client,
+      fallbackHash,
+      parameters?.pollingInterval,
+      parameters?.maxRetries
+    );
   } finally {
     statusApiWebSocket.unsubscribe(taskId);
     statusApiWebSocket.offUpdate(updateHandler);
diff --git a/src/relay/index.ts b/src/relay/index.ts
index 66be8a92..09646bd1 100644
--- a/src/relay/index.ts
+++ b/src/relay/index.ts
@@ -1,8 +1,13 @@
-import type { SignedAuthorizationList } from "viem";
+import type { Account, Chain, SignedAuthorizationList, Transport } from "viem";
 
+import type { GelatoWalletClient } from "../actions/index.js";
 import { api } from "../constants/index.js";
 import { on, wait } from "./actions/index.js";
-import type { GelatoTaskEvent, TransactionStatusResponse } from "./status/index.js";
+import type {
+  GelatoTaskEvent,
+  GelatoTaskWaitEvent,
+  TransactionStatusResponse
+} from "./status/index.js";
 
 interface BaseCallRequest {
   chainId: number;
@@ -23,15 +28,25 @@ export interface SmartWalletCallRequest extends BaseCallRequest {
 }
 
 export interface GelatoResponse {
+  /// Task ID
   id: string;
-  wait: () => Promise<string>;
+  /// Wait for the task to be executed or submitted on chain
+  wait: (e?: GelatoTaskWaitEvent) => Promise<string>;
+  /// Subscribe for task updates
   on(update: GelatoTaskEvent, callback: (parameter: TransactionStatusResponse) => void): () => void;
+  /// Subscribe for task errors
   on(update: "error", callback: (parameter: Error) => void): () => void;
 }
 
-const callGelatoApi = async <T extends object>(
+const callGelatoApi = async <
+  T extends object,
+  transport extends Transport = Transport,
+  chain extends Chain = Chain,
+  account extends Account = Account
+>(
   endpoint: string,
-  request: T
+  request: T,
+  client: GelatoWalletClient<transport, chain, account>
 ): Promise<GelatoResponse> => {
   if (
     "authorizationList" in request &&
@@ -54,13 +69,25 @@ const callGelatoApi = async <T extends object>(
 
   return {
     id: taskId,
-    wait: () => wait(taskId),
+    wait: (e: GelatoTaskWaitEvent = "execution") => wait(taskId, client, e === "submission"),
     on: (update: GelatoTaskEvent | "error", callback) => on(taskId, { update, callback })
   };
 };
 
-export const sponsoredCall = (request: SponsoredCallRequest): Promise<GelatoResponse> =>
-  callGelatoApi("/relays/v2/sponsored-call-eip7702", request);
+export const sponsoredCall = <
+  transport extends Transport = Transport,
+  chain extends Chain = Chain,
+  account extends Account = Account
+>(
+  request: SponsoredCallRequest,
+  client: GelatoWalletClient<transport, chain, account>
+): Promise<GelatoResponse> => callGelatoApi("/relays/v2/sponsored-call-eip7702", request, client);
 
-export const smartWalletCall = (request: SmartWalletCallRequest): Promise<GelatoResponse> =>
-  callGelatoApi("/relays/v2/smart-wallet-call", request);
+export const smartWalletCall = <
+  transport extends Transport = Transport,
+  chain extends Chain = Chain,
+  account extends Account = Account
+>(
+  request: SmartWalletCallRequest,
+  client: GelatoWalletClient<transport, chain, account>
+): Promise<GelatoResponse> => callGelatoApi("/relays/v2/smart-wallet-call", request, client);
diff --git a/src/relay/status/index.ts b/src/relay/status/index.ts
index ebbf5f60..88c05f84 100644
--- a/src/relay/status/index.ts
+++ b/src/relay/status/index.ts
@@ -7,5 +7,6 @@ export {
   ExecutionCancelledError,
   ExecutionRevertedError,
   InternalError,
-  GelatoTaskEvent
+  GelatoTaskEvent,
+  GelatoTaskWaitEvent
 } from "./types.js";
diff --git a/src/relay/status/types.ts b/src/relay/status/types.ts
index 64401404..0455b066 100644
--- a/src/relay/status/types.ts
+++ b/src/relay/status/types.ts
@@ -43,7 +43,9 @@ export interface ErrorWebsocketMessage {
   payload: Error;
 }
 
-export type GelatoTaskEvent = "success" | "cancel" | "revert";
+export type GelatoTaskWaitEvent = "execution" | "submission";
+
+export type GelatoTaskEvent = "success" | "submitted" | "cancel" | "revert";
 
 export class GelatoTaskError extends Error {
   readonly taskId: string;
diff --git a/src/relay/status/utils.ts b/src/relay/status/utils.ts
index 49424033..359f27f4 100644
--- a/src/relay/status/utils.ts
+++ b/src/relay/status/utils.ts
@@ -10,3 +10,7 @@ export const isFinalTaskState = (taskState: TaskState): boolean => {
       return false;
   }
 };
+
+export const isSubmitted = (taskState: TaskState): boolean => {
+  return taskState === TaskState.ExecPending || taskState === TaskState.WaitingForConfirmation;
+};


Reviews:
user: login: pedrocrvz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T12:48:49Z
user: login: mkykadir
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T13:36:18Z
user: login: bennoprice
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T14:08:57Z
user: login: pedrocrvz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T14:16:09Z
user: login: pedrocrvz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T14:46:18Z
user: login: pedrocrvz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T14:50:48Z
user: login: mkykadir
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T06:33:01Z
user: login: pedrocrvz
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T07:52:40Z
user: login: pedrocrvz
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T10:17:52Z
user: login: mkykadir
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T10:31:35Z
user: login: pedrocrvz
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T12:57:40Z
user: login: bennoprice
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T16:22:16Z
user: login: pedrocrvz
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T16:30:31Z

Review comments:
diff_hunk: @@ -23,9 +23,15 @@ export interface SmartWalletCallRequest extends BaseCallRequest {
 }
 
 export interface GelatoResponse {
+  /// Task ID
   id: string;
+  /// Wait for the task to be executed on chain
   wait: () => Promise<string>;
+  /// Wait for the task to be submitted to chain
+  waitSubmission: () => Promise<string>;
path: src/relay/index.ts
user: login: pedrocrvz
body: For interface simplicity, I suggest to just maintain `wait: (e: "inclusion" | "submission" = "inclusion")`
created_at: 2025-05-06T12:48:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -23,9 +23,15 @@ export interface SmartWalletCallRequest extends BaseCallRequest {
 }
 
 export interface GelatoResponse {
+  /// Task ID
   id: string;
+  /// Wait for the task to be executed on chain
   wait: () => Promise<string>;
+  /// Wait for the task to be submitted to chain
+  waitSubmission: () => Promise<string>;
path: src/relay/index.ts
user: login: mkykadir
body: refactored in c36de3565c0dd86be0ff602d7d22861310740b00
created_at: 2025-05-07T13:36:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -11,6 +11,11 @@ import { http, useAccount } from "wagmi";
 
 import { sepolia } from "viem/chains";
 
+enum TransactionStatus {
+  SUBMITTED = "submitted",
+  EXECUTED = "executed"
path: examples/react-vite/src/providers.tsx
user: login: pedrocrvz
body: nitpick: let's not use all caps, rather `Executed` and `Submitted`
created_at: 2025-05-07T14:16:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -33,4 +33,12 @@ export const waitHttp = async (taskId: string): Promise<Hash | undefined> => {
 
     throw new InternalError(taskId);
   }
+
+  if (
+    submission &&
+    (taskStatus.taskState === TaskState.ExecPending ||
path: src/relay/actions/internal/waitHttp.ts
user: login: pedrocrvz
body: let's replace the second condition with a `isSubmitted` helper. please also replace other parts of code where we verify this condition
created_at: 2025-05-07T14:46:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -23,9 +27,13 @@ export interface SmartWalletCallRequest extends BaseCallRequest {
 }
 
 export interface GelatoResponse {
+  /// Task ID
   id: string;
-  wait: () => Promise<string>;
+  /// Wait for the task to be executed or submitted on chain
+  wait: (e: GelatoTaskWaitEvent) => Promise<string>;
+  /// Subscribe for task updates
   on(update: GelatoTaskEvent, callback: (parameter: TransactionStatusResponse) => void): () => void;
+  /// Subscribe for task errors
   on(update: "error", callback: (parameter: Error) => void): () => void;
path: src/relay/index.ts
user: login: pedrocrvz
body: can we use `GelatoTaskError` type here?
created_at: 2025-05-07T14:50:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -23,9 +27,13 @@ export interface SmartWalletCallRequest extends BaseCallRequest {
 }
 
 export interface GelatoResponse {
+  /// Task ID
   id: string;
-  wait: () => Promise<string>;
+  /// Wait for the task to be executed or submitted on chain
+  wait: (e: GelatoTaskWaitEvent) => Promise<string>;
+  /// Subscribe for task updates
   on(update: GelatoTaskEvent, callback: (parameter: TransactionStatusResponse) => void): () => void;
+  /// Subscribe for task errors
   on(update: "error", callback: (parameter: Error) => void): () => void;
path: src/relay/index.ts
user: login: mkykadir
body: this can be any WS error actually
created_at: 2025-05-08T06:32:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -33,4 +33,12 @@ export const waitHttp = async (taskId: string): Promise<Hash | undefined> => {
 
     throw new InternalError(taskId);
   }
+
+  if (
+    submission &&
+    (taskStatus.taskState === TaskState.ExecPending ||
path: src/relay/actions/internal/waitHttp.ts
user: login: mkykadir
body: refactored in 3b1773bb121ed5af9653326b640829b696bdbcc6
created_at: 2025-05-08T06:32:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -11,6 +11,11 @@ import { http, useAccount } from "wagmi";
 
 import { sepolia } from "viem/chains";
 
+enum TransactionStatus {
+  SUBMITTED = "submitted",
+  EXECUTED = "executed"
path: examples/react-vite/src/providers.tsx
user: login: mkykadir
body: refactored in 33ebe6f723f831e827fa5916e65594c6de938ddf
created_at: 2025-05-08T06:32:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -67,7 +88,33 @@ export const wait = async (
     statusApiWebSocket.onError(errorHandler);
 
     await statusApiWebSocket.subscribe(taskId);
-    return await promise;
+
+    // Listen for task status, when submission event is received but we're waiting for the executionn
+    // `promise` will resolve with `waitForReceipt` set to true and Provider's TX receipt will race with
+    // status API to fetch inclusion as quickly as possible
+    const result = await promise;
+    while (result.waitForReceipt && !submission) {
+      const promise = new Promise<TaskStatusReturn>((resolve, reject) => {
+        resolvePromise = resolve;
+        rejectPromise = reject;
+      });
+
+      const _result = await Promise.race([
+        promise,
+        client.waitForTransactionReceipt({ hash: result.hash })
path: src/relay/actions/wait.ts
user: login: pedrocrvz
body: let's pass a lower `pollingInterval` here like `1000` ms, default is `4000` ms
created_at: 2025-05-08T10:17:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -67,7 +88,33 @@ export const wait = async (
     statusApiWebSocket.onError(errorHandler);
 
     await statusApiWebSocket.subscribe(taskId);
-    return await promise;
+
+    // Listen for task status, when submission event is received but we're waiting for the executionn
+    // `promise` will resolve with `waitForReceipt` set to true and Provider's TX receipt will race with
+    // status API to fetch inclusion as quickly as possible
+    const result = await promise;
+    while (result.waitForReceipt && !submission) {
+      const promise = new Promise<TaskStatusReturn>((resolve, reject) => {
+        resolvePromise = resolve;
+        rejectPromise = reject;
+      });
+
+      const _result = await Promise.race([
+        promise,
+        client.waitForTransactionReceipt({ hash: result.hash })
path: src/relay/actions/wait.ts
user: login: mkykadir
body: added in 2dc4454f3dc23801fe219217211359d723515aa7
created_at: 2025-05-08T10:31:34Z
author_association: CONTRIBUTOR

PR comments:
user: login: pedrocrvz
created_at: 2025-05-06T12:52:31Z
body: Are we able to get updates via ws if the transaction hash changes? (i.e there's a resubmission)
author_association: CONTRIBUTOR
user: login: mkykadir
created_at: 2025-05-07T13:42:11Z
body: > Are we able to get updates via ws if the transaction hash changes? (i.e there's a resubmission)

yes we are since resubmissions are on the same flow, new TX hash will be published as Execution proof so WS should trigger with the updated hash
author_association: CONTRIBUTOR
user: login: mkykadir
created_at: 2025-05-08T09:29:04Z
body: @pedrocrvz 7eca514b59e6aaa4a5e993ef0658d72f03ab5d3b adds the provider tx receipt check with resubmission handling
author_association: CONTRIBUTOR