Skip to content

feat: add order submission block tracking and improve order ID retrieval logic#379

Merged
onahprosper merged 3 commits intomainfrom
fix-noblocks-stuck-on-confirming
Feb 23, 2026
Merged

feat: add order submission block tracking and improve order ID retrieval logic#379
onahprosper merged 3 commits intomainfrom
fix-noblocks-stuck-on-confirming

Conversation

@Dprof-in-tech
Copy link
Copy Markdown
Collaborator

@Dprof-in-tech Dprof-in-tech commented Feb 23, 2026

Description

This pull request enhances the transaction workflow in TransactionPreview.tsx by improving the reliability of order log retrieval and ensuring more accurate block tracking during transaction submissions. The main changes revolve around capturing the blockchain block number at the moment of transaction submission and using it to optimize event log polling, which should reduce missed events and unnecessary polling.

Key improvements include:

Block Tracking and Submission:

  • Introduced an orderSubmissionBlock ref to store the block number at the time of transaction submission, and added a captureSubmissionBlock function to retrieve and set this block number before sending transactions. This ensures the system knows exactly from which block to start searching for relevant logs. [1] [2] [3] [4] [5] [6]

Order Log Polling Improvements:

  • Refactored the getOrderId function to use the captured submission block as the starting point (fromBlock) for polling order creation logs, rather than using a fixed block range. This increases accuracy and efficiency in log retrieval. [1] [2]
  • Improved polling logic to prevent duplicate processing by tracking when a log is found and properly clearing the polling interval. [1] [2] [3]

References

closes #355

Testing

  • This change adds test coverage for new/changed/fixed functionality

Checklist

  • I have added documentation and tests for new/changed functionality in this PR
  • All active GitHub checks for tests, formatting, and security are passing
  • The correct base branch is being used, if not main

By submitting a PR, I agree to Paycrest's Contributor Code of Conduct and Contribution Guide.

Summary by CodeRabbit

  • Refactor

    • Unified and enhanced order submission tracking and polling with a steady 2s interval for more reliable detection.
  • Bug Fixes

    • Improved error handling and added logging around transaction polling and detection.
  • Improvements

    • More accurate order status detection by capturing the submission block across multiple wallet paths, reducing missed or delayed order recognition.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Feb 23, 2026

📝 Walkthrough

Walkthrough

Capture the RPC block number at transaction submission and rework order polling to use that block as a lower bound. Polling runs every 2s, queries OrderCreated logs between computed bounds, resolves when a matching log is found, updates order state, and is invoked before all submission paths.

Changes

Cohort / File(s) Summary
Order polling & submission block capture
app/pages/TransactionPreview.tsx
Added useRef orderSubmissionBlock and captureSubmissionBlock() to record RPC block at submission. Reworked polling: getOrderId uses an internal poll loop (2s interval), computes fromBlock from the captured block (fallback to toBlock - 10), queries OrderCreated logs in bounded ranges, resolves when found, updates state (orderId, createdAt, status, currentStep) and saves tx data. Injected capture before submission across injected wallet, EIP-7702 (MEE), and smart-wallet flows. Added error handling and console logs. Component signature unchanged.

Sequence Diagram

sequenceDiagram
    participant Component as TransactionPreview
    participant RPC as Public Client (RPC)
    participant Submit as Transaction Submitter
    participant Poller as Poll Loop (2s)
    participant Logs as Log Retrieval
    participant State as State Updater

    Component->>RPC: captureSubmissionBlock()
    RPC-->>Component: blockNumber
    Component->>Component: store orderSubmissionBlock.current

    Component->>Submit: send transaction (approve/create/execute)
    Submit-->>Component: tx hash / confirmation

    loop every 2s
        Poller->>Poller: determine fromBlock (orderSubmissionBlock || toBlock-10)
        Poller->>Logs: query OrderCreated logs in [fromBlock, toBlock]
        Logs-->>Poller: logs or empty
        alt log found
            Poller->>State: update orderId, createdAt, status, currentStep
            State-->>Poller: resolved
            Poller-->>Poller: stop polling
        else not found
            Poller-->>Poller: continue
        end
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I watched the chain and caught the block,
Saved the number — tick-tock, tick-tock.
I poll in bounds, I hop, I find,
Order logs no longer blind.
Tiny paws, confirmations unlocked 🥕

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main changes: adding order submission block tracking and improving order ID retrieval logic, which aligns with the primary objectives of the PR.
Description check ✅ Passed The PR description covers the purpose, key improvements, and references issue #355, but the Testing section is empty with the checkbox unchecked, and documentation/test checklist items remain unchecked.
Linked Issues check ✅ Passed The PR successfully addresses issue #355 by capturing submission blocks and improving log polling to detect transaction status changes faster, reducing the duration transactions remain in 'Confirming' state.
Out of Scope Changes check ✅ Passed All changes are scoped to TransactionPreview.tsx and directly support the objective of improving order log retrieval and block tracking with no extraneous modifications.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix-noblocks-stuck-on-confirming

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
app/pages/TransactionPreview.tsx (1)

624-627: New createPublicClient allocated on every poll tick

A fresh client is constructed every 2 seconds for the lifetime of polling. Hoist it once outside poll to avoid unnecessary object allocation and potential connection overhead.

♻️ Proposed refactor
 const poll = async () => {
   if (found || !activeWallet?.address) return;
   try {
-    const publicClient = createPublicClient({
-      chain: selectedNetwork.chain,
-      transport: http(getRpcUrl(selectedNetwork.chain.name)),
-    });
     const toBlock = await publicClient.getBlockNumber();

And before the poll definition:

+    const publicClient = createPublicClient({
+      chain: selectedNetwork.chain,
+      transport: http(getRpcUrl(selectedNetwork.chain.name)),
+    });
+
     const poll = async () => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/pages/TransactionPreview.tsx` around lines 624 - 627, The code creates a
new publicClient on every poll tick; move the createPublicClient(...) call out
of the poll function and hoist it before the poll definition (e.g., create a
single publicClient with the selectedNetwork info once) and then use that
instance inside poll; if selectedNetwork can change during component lifetime,
create the client with useMemo or recreate it in an effect keyed on
selectedNetwork (refer to createPublicClient, poll, and selectedNetwork.chain)
so you avoid allocating a new client every 2s.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/pages/TransactionPreview.tsx`:
- Around line 617-618: The polling interval created inside the getOrderId
promise (the local intervalId variable used with setInterval) can continue after
the component unmounts; move the interval ID into a ref (e.g., intervalIdRef) so
the interval is accessible outside the closure, clear that ref in a useEffect
cleanup, and also clear the ref/interval when polling resolves or errors (inside
the getOrderId promise). Apply the same change for the other polling instance
referenced around the second setInterval (the one at the 677-678 region) so all
intervals are stored in refs and reliably cleared on unmount or completion.
- Around line 615-679: The getOrderId promise in getOrderId never rejects or
times out, causing the UI to stay stuck; add a clear failure path by introducing
a maxRetry counter or wall-clock timeout inside getOrderId (track attempts or
use setTimeout) that, when exceeded, clears the intervalId, sets found to true
(or otherwise stops polling), calls reject(new Error(...)) so the caller
(createOrder) can handle it, and optionally set isConfirming to false / update
state; ensure you reference and clear intervalId, handle the rejection where
getOrderId is awaited, and keep the existing success flow (decodeEventLog,
setOrderId, saveTransactionData) intact.

---

Nitpick comments:
In `@app/pages/TransactionPreview.tsx`:
- Around line 624-627: The code creates a new publicClient on every poll tick;
move the createPublicClient(...) call out of the poll function and hoist it
before the poll definition (e.g., create a single publicClient with the
selectedNetwork info once) and then use that instance inside poll; if
selectedNetwork can change during component lifetime, create the client with
useMemo or recreate it in an effect keyed on selectedNetwork (refer to
createPublicClient, poll, and selectedNetwork.chain) so you avoid allocating a
new client every 2s.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 858c1c3 and 65cc8f2.

📒 Files selected for processing (1)
  • app/pages/TransactionPreview.tsx

Comment thread app/pages/TransactionPreview.tsx
Comment thread app/pages/TransactionPreview.tsx Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
app/pages/TransactionPreview.tsx (2)

220-230: createPublicClient is re-instantiated on every poll tick — hoist it.

captureSubmissionBlock (line 222) and the poll closure (line 642) each instantiate a fresh HTTP public client using the same chain + transport config. poll runs up to 60 times over the 120 s window, so this spawns up to 60 unnecessary client objects.

Create the client once — either at the module level, in a useMemo, or at minimum once at the top of getOrderId's Promise constructor — and pass or close over it:

♻️ Proposed refactor
-  const captureSubmissionBlock = async () => {
+  const getPublicClient = () =>
+    createPublicClient({
+      chain: selectedNetwork.chain,
+      transport: http(getRpcUrl(selectedNetwork.chain.name)),
+    });
+
+  const captureSubmissionBlock = async () => {
     try {
-      const publicClient = createPublicClient({
-        chain: selectedNetwork.chain,
-        transport: http(getRpcUrl(selectedNetwork.chain.name)),
-      });
+      const publicClient = getPublicClient();
       orderSubmissionBlock.current = await publicClient.getBlockNumber();
     } catch {
       orderSubmissionBlock.current = null;
     }
   };
   return new Promise<void>((resolve, reject) => {
+    const publicClient = getPublicClient();
     let intervalId: NodeJS.Timeout;
     ...
     const poll = async () => {
       ...
-      const publicClient = createPublicClient({
-        chain: selectedNetwork.chain,
-        transport: http(getRpcUrl(selectedNetwork.chain.name)),
-      });
       const toBlock = await publicClient.getBlockNumber();

Also applies to: 642-645

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/pages/TransactionPreview.tsx` around lines 220 - 230,
captureSubmissionBlock and the poll closure both call createPublicClient
repeatedly (creating up to 60 clients); hoist the public client creation so a
single instance is reused: create the client once (e.g., via useMemo or at the
start of getOrderId's Promise) using selectedNetwork.chain and
http(getRpcUrl(selectedNetwork.chain.name)), then have captureSubmissionBlock,
poll, and orderSubmissionBlock.current reference that shared client instead of
calling createPublicClient each time; remove the duplicate createPublicClient
calls in the poll closure and ensure the shared client is in scope where
getBlockNumber is invoked.

651-678: decodeEventLog re-decodes what getContractEvents already decoded — use logs[0].args directly, and add strict: true.

getContractEvents with a specific eventName returns logs with .args already decoded via the same ABI. Calling decodeEventLog again on logs[0].data / logs[0].topics is redundant. Additionally, without strict: true, logs[0].args (and the duplicate decode) returns fields as optional — orderId could be undefined, silently breaking setOrderId and saveTransactionData.

♻️ Proposed refactor
           const logs = await publicClient.getContractEvents({
             address: getGatewayContractAddress(
               selectedNetwork.chain.name,
             ) as `0x${string}`,
             abi: gatewayAbi,
             eventName: "OrderCreated",
             args: {
               sender: activeWallet.address as `0x${string}`,
               token: tokenAddress,
               amount: parseUnits(amountSent.toString(), tokenDecimals ?? 18),
             },
             fromBlock,
             toBlock,
+            strict: true,
           });

           if (logs.length > 0 && !settled) {
             settled = true;
-            cleanup();
-
-            const decodedLog = decodeEventLog({
-              abi: gatewayAbi,
-              eventName: "OrderCreated",
-              data: logs[0].data,
-              topics: logs[0].topics,
-            });
-
-            setIsOrderCreatedLogsFetched(true);
-            setOrderId(decodedLog.args.orderId);
-
-            await saveTransactionData({
-              orderId: decodedLog.args.orderId,
-              txHash: logs[0].transactionHash,
-            });
+            try {
+              setIsOrderCreatedLogsFetched(true);
+              setOrderId(logs[0].args.orderId);
+
+              await saveTransactionData({
+                orderId: logs[0].args.orderId,
+                txHash: logs[0].transactionHash,
+              });
             setCreatedAt(new Date().toISOString());
             setTransactionStatus("pending");
             setCurrentStep("status");
             resolve();
+            } catch (innerError) {
+              reject(innerError);
+            } finally {
+              cleanup();
+            }
           }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/pages/TransactionPreview.tsx` around lines 651 - 678, The code
redundantly re-decodes events and risks undefined fields; update the
publicClient.getContractEvents call to pass strict: true so args are
non-optional, stop using decodeEventLog, and read the decoded values directly
from logs[0].args (e.g. use logs[0].args.orderId) when setting state via
setOrderId and setIsOrderCreatedLogsFetched; also add a simple presence check on
logs[0].args.orderId before calling setOrderId to avoid silently setting
undefined.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/pages/TransactionPreview.tsx`:
- Around line 666-689: The success branch currently calls cleanup() before
running decodeEventLog and other steps, so if decodeEventLog throws the promise
never settles; update the block around
decodeEventLog/saveTransactionData/resolve to ensure cleanup always runs and the
promise is settled on errors: either wrap the inner success logic
(decodeEventLog, setIsOrderCreatedLogsFetched, setOrderId, await
saveTransactionData, setCreatedAt, setTransactionStatus, setCurrentStep,
resolve) in a try/catch/finally where cleanup() is called in finally and any
exception calls reject(err) (or move cleanup() into a finally and keep resolve()
on success), ensuring decodeEventLog errors lead to reject() and the
timeout/interval are still cleared.

---

Nitpick comments:
In `@app/pages/TransactionPreview.tsx`:
- Around line 220-230: captureSubmissionBlock and the poll closure both call
createPublicClient repeatedly (creating up to 60 clients); hoist the public
client creation so a single instance is reused: create the client once (e.g.,
via useMemo or at the start of getOrderId's Promise) using selectedNetwork.chain
and http(getRpcUrl(selectedNetwork.chain.name)), then have
captureSubmissionBlock, poll, and orderSubmissionBlock.current reference that
shared client instead of calling createPublicClient each time; remove the
duplicate createPublicClient calls in the poll closure and ensure the shared
client is in scope where getBlockNumber is invoked.
- Around line 651-678: The code redundantly re-decodes events and risks
undefined fields; update the publicClient.getContractEvents call to pass strict:
true so args are non-optional, stop using decodeEventLog, and read the decoded
values directly from logs[0].args (e.g. use logs[0].args.orderId) when setting
state via setOrderId and setIsOrderCreatedLogsFetched; also add a simple
presence check on logs[0].args.orderId before calling setOrderId to avoid
silently setting undefined.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 65cc8f2 and 2179228.

📒 Files selected for processing (1)
  • app/pages/TransactionPreview.tsx

Comment thread app/pages/TransactionPreview.tsx
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
app/pages/TransactionPreview.tsx (2)

220-230: createPublicClient duplicated across captureSubmissionBlock and poll — consider sharing one instance

captureSubmissionBlock creates a client and throws it away; poll then creates a second client (on every tick) for the same chain and transport. Returning the client from captureSubmissionBlock and threading it into getOrderId would eliminate the per-tick allocation:

♻️ Proposed refactor
-  const captureSubmissionBlock = async () => {
+  const captureSubmissionBlock = async (): Promise<ReturnType<typeof createPublicClient> | null> => {
     try {
       const publicClient = createPublicClient({
         chain: selectedNetwork.chain,
         transport: http(getRpcUrl(selectedNetwork.chain.name)),
       });
       orderSubmissionBlock.current = await publicClient.getBlockNumber();
+      return publicClient;
     } catch {
       orderSubmissionBlock.current = null;
+      return null;
     }
   };

Then in each call site:

-  await captureSubmissionBlock();
-  // ...
-  await getOrderId();
+  const submissionClient = await captureSubmissionBlock();
+  // ...
+  await getOrderId(submissionClient);

And inside getOrderId, accept the optional client and skip createPublicClient when provided.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/pages/TransactionPreview.tsx` around lines 220 - 230,
captureSubmissionBlock creates and discards a new public client while poll
creates its own client on every tick, causing redundant client allocations;
refactor captureSubmissionBlock to create and return the public client (created
via createPublicClient with selectedNetwork.chain and http(getRpcUrl(...))) and
propagate that client into getOrderId and poll so they reuse the same instance;
update getOrderId signature to accept an optional public client and skip calling
createPublicClient when one is provided, and ensure orderSubmissionBlock.current
is still set inside captureSubmissionBlock (or by the caller) so existing logic
relying on orderSubmissionBlock.current remains intact.

638-645: createPublicClient re-instantiated on every 2-second poll tick — hoist it to the Promise body

A fresh client is allocated on each of up to 60 poll invocations. Moving it above the poll definition reuses one instance for the entire polling lifetime:

♻️ Proposed refactor
   return new Promise<void>((resolve, reject) => {
     let intervalId: NodeJS.Timeout;
     let timeoutId: NodeJS.Timeout;
     let settled = false;

+    const publicClient = createPublicClient({
+      chain: selectedNetwork.chain,
+      transport: http(getRpcUrl(selectedNetwork.chain.name)),
+    });

     const cleanup = () => { ... };
     timeoutId = setTimeout(...);

     const poll = async () => {
       if (settled || !activeWallet?.address) return;
       try {
-        const publicClient = createPublicClient({
-          chain: selectedNetwork.chain,
-          transport: http(getRpcUrl(selectedNetwork.chain.name)),
-        });
         const toBlock = await publicClient.getBlockNumber();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/pages/TransactionPreview.tsx` around lines 638 - 645, The poll function
currently calls createPublicClient on every tick, allocating a new client each
time; hoist the client creation into the outer Promise body (before defining
poll) so a single publicClient instance is reused for the whole polling
lifecycle: createPublicClient({ chain: selectedNetwork.chain, transport:
http(getRpcUrl(selectedNetwork.chain.name)) }) once in the Promise scope, keep a
reference named publicClient, then remove its instantiation from inside poll and
have poll use that outer publicClient; ensure selectedNetwork and getRpcUrl are
available in the Promise scope and that any error handling around client
creation remains intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/pages/TransactionPreview.tsx`:
- Around line 647-649: In getOrderId, avoid using the latest toBlock as the
fallback floor (toBlock - BigInt(10)) because that narrows the search window;
instead capture a pollStartBlock once at the top of getOrderId by calling
publicClient.getBlockNumber() and use that as the floor when computing
fromBlock: set fromBlock = orderSubmissionBlock.current ?? (pollStartBlock -
BigInt(10)). Update references to publicClient.getBlockNumber() in getOrderId so
you compute pollStartBlock once, keep the later toBlock for the current head,
and use pollStartBlock as the fallback to ensure the scan window covers blocks
present at the start of the polling cycle.
- Around line 647-649: Compute and store a single "poll start" block number once
at the top of the Promise (e.g., read publicClient.getBlockNumber() into
pollStartBlock) and then use that stored pollStartBlock as the floor when
computing fromBlock (i.e., set fromBlock to orderSubmissionBlock.current ??
pollStartBlock - BigInt(10)) instead of re-reading toBlock each poll; update the
code around toBlock, fromBlock, and orderSubmissionBlock.current so toBlock
remains the latest block but the fallback lower bound is the initial
pollStartBlock to avoid a shrinking window.
- Around line 681-684: The call to saveTransactionData passes
logs[0].transactionHash which is typed nullable (Log.transactionHash | null);
update the code around saveTransactionData to guard or assert the value: check
that logs[0] exists and logs[0].transactionHash is non-null before calling
saveTransactionData (e.g., if (logs[0]?.transactionHash) { await
saveTransactionData({ orderId: decodedLog.args.orderId, txHash:
logs[0].transactionHash }); }), or if you can guarantee non-null, use a type
assertion on logs[0].transactionHash as 0x${string}; ensure you reference logs,
decodedLog.args.orderId and saveTransactionData in the fix.

---

Nitpick comments:
In `@app/pages/TransactionPreview.tsx`:
- Around line 220-230: captureSubmissionBlock creates and discards a new public
client while poll creates its own client on every tick, causing redundant client
allocations; refactor captureSubmissionBlock to create and return the public
client (created via createPublicClient with selectedNetwork.chain and
http(getRpcUrl(...))) and propagate that client into getOrderId and poll so they
reuse the same instance; update getOrderId signature to accept an optional
public client and skip calling createPublicClient when one is provided, and
ensure orderSubmissionBlock.current is still set inside captureSubmissionBlock
(or by the caller) so existing logic relying on orderSubmissionBlock.current
remains intact.
- Around line 638-645: The poll function currently calls createPublicClient on
every tick, allocating a new client each time; hoist the client creation into
the outer Promise body (before defining poll) so a single publicClient instance
is reused for the whole polling lifecycle: createPublicClient({ chain:
selectedNetwork.chain, transport: http(getRpcUrl(selectedNetwork.chain.name)) })
once in the Promise scope, keep a reference named publicClient, then remove its
instantiation from inside poll and have poll use that outer publicClient; ensure
selectedNetwork and getRpcUrl are available in the Promise scope and that any
error handling around client creation remains intact.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2179228 and 7ae8f8a.

📒 Files selected for processing (1)
  • app/pages/TransactionPreview.tsx

Comment thread app/pages/TransactionPreview.tsx
Comment thread app/pages/TransactionPreview.tsx
@onahprosper onahprosper merged commit a452fae into main Feb 23, 2026
1 check passed
@onahprosper onahprosper deleted the fix-noblocks-stuck-on-confirming branch February 23, 2026 17:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Transactions Stuck in "Confirming" State for long

2 participants