Skip to content

Implement transaction watcher timer and follow-up scan logic#690

Open
kartikangiras wants to merge 1 commit intobitcoinppl:masterfrom
kartikangiras:ticktrait
Open

Implement transaction watcher timer and follow-up scan logic#690
kartikangiras wants to merge 1 commit intobitcoinppl:masterfrom
kartikangiras:ticktrait

Conversation

@kartikangiras
Copy link
Copy Markdown
Contributor

@kartikangiras kartikangiras commented Apr 19, 2026

Summary

Refactored actor timer implementations to use act-zero trait and Timer infrastructure instead of manual send_fut + tokio::time::sleep patterns.

fixes #495

Testing

  • just fmt
  • just ci

Platform Coverage

  • Tested on iOS device
  • Tested on Android device
  • Tested on iOS simulator
  • Tested on Android simulator
  • Not tested

Checklist

Summary by CodeRabbit

  • Refactor
    • Replaced per-transaction delayed tasks with a single actor-level timer that batches follow-up checks, reducing concurrent timers and improving efficiency and predictability of follow-up scans.
    • Converted transaction watching to a timer/tick-driven model that caches the node client, triggers an immediate initial check, and centralizes scheduling and backoff to provide more reliable, resource-friendly monitoring and clearer retry behavior.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 19, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.18% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed Title accurately summarizes the main changes: implementing transaction watcher timer logic and follow-up scan refactoring using act-zero Tick trait.
Description check ✅ Passed Description includes summary, testing details, platform coverage checklist, and contributor checklist matching the template structure completely.
Linked Issues check ✅ Passed Changes comprehensively implement issue #495 objectives: TransactionWatcher now uses Tick trait with Timer infrastructure [#495], WalletManager refactored to use pending_txids queue and Timer for delayed scans [#495], removing manual send_fut + sleep patterns throughout.
Out of Scope Changes check ✅ Passed All changes align with issue #495 scope: TransactionWatcher Tick implementation, WalletManager timer refactoring, and actor method visibility updates directly support the stated objectives.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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
Review rate limit: 0/1 reviews remaining, refill in 60 minutes.

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

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 19, 2026

Greptile Summary

This PR refactors the transaction polling loop in TransactionWatcher and the follow-up scan logic in WalletActor to use act-zero's Timer/Tick infrastructure instead of manual send_fut + tokio::time::sleep patterns, eliminating raw async task spawning in favour of actor-native scheduling.

  • The shared scan_timer in WalletActor means transactions found after the timer is already armed (within the 30-second window) receive less than the full 30-second follow-up delay, weakening the intended sanity check.
  • If mark_transaction_found returns an error before pushing to pending_follow_up_scans, remove_watcher_for_txn is never called, leaving the (now-idle) TransactionWatcher actor allocated indefinitely.

Confidence Score: 4/5

Safe to merge with awareness of the coalesced-timer and watcher-leak edge cases, both of which only manifest when multiple transactions are confirmed in close succession or when a scan errors out.

Both findings are P2: they affect uncommon edge cases (concurrent confirms, error paths) and are not regressions that break the primary flow. The core refactor is structurally sound and improves actor hygiene. Score is 4 rather than 5 because the timer-coalescing issue is a correctness deviation from the stated intent of the 30-second delay.

rust/src/manager/wallet_manager/actor.rs (shared timer logic), rust/src/transaction_watcher.rs (watcher cleanup on error path)

Important Files Changed

Filename Overview
rust/src/manager/wallet_manager/actor.rs Added scan_timer, pending_follow_up_scans, and pending_scan_timer_armed fields plus a Tick implementation; refactored mark_transaction_found to use a shared 30-second timer instead of individual send_fut+sleep. The shared timer can give later-arriving transactions less than the intended 30-second follow-up delay.
rust/src/transaction_watcher.rs Replaced the self-contained send_fut polling loop with a Tick-based timer. client and normal_wait_time are now stored as fields; first check is triggered immediately with Duration::ZERO. After finding a transaction the timer is cleared but the actor may leak if mark_transaction_found errors out before scheduling the deferred watcher removal.

Sequence Diagram

sequenceDiagram
    participant TW as TransactionWatcher
    participant WA as WalletActor
    participant Timer_TW as TW Timer
    participant Timer_WA as WA scan_timer

    TW->>TW: start_watching() — build client, store in self.client
    TW->>Timer_TW: set_timeout(Duration::ZERO)
    Timer_TW-->>TW: tick()
    TW->>TW: check_txn()
    alt Transaction not found
        TW->>Timer_TW: set_timeout(normal_wait_time)
        Timer_TW-->>TW: tick() [repeat]
    else Error
        TW->>Timer_TW: set_timeout(30s)
    else Transaction found
        TW->>WA: send!(mark_transaction_found(tx_id))
        TW->>Timer_TW: clear()
        WA->>WA: update_block_id()
        WA->>WA: perform_scan_for_single_tx_id(tx_id)
        WA->>WA: pending_follow_up_scans.push(tx_id)
        alt Timer not yet armed
            WA->>Timer_WA: set_timeout_for_weak(30s)
            WA->>WA: pending_scan_timer_armed = true
        end
        Timer_WA-->>WA: tick()
        WA->>WA: pending_scan_timer_armed = false
        loop for each tx_id in pending_follow_up_scans
            WA->>WA: send!(perform_scan_for_single_tx_id)
            WA->>WA: send!(remove_watcher_for_txn)
        end
    end
Loading

Reviews (1): Last reviewed commit: "Implement transaction watcher timer and ..." | Re-trigger Greptile

Comment thread rust/src/manager/wallet_manager/actor.rs
Comment thread rust/src/transaction_watcher.rs
Copy link
Copy Markdown

@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 (3)
rust/src/transaction_watcher.rs (2)

60-64: Collapse duplicate match arms.

Bitcoin and Testnet | Testnet4 map to the same Duration::from_secs(20).

♻️ Proposed simplification
             normal_wait_time: match network {
-                Network::Bitcoin => Duration::from_secs(20),
-                Network::Testnet | Network::Testnet4 => Duration::from_secs(20),
+                Network::Bitcoin | Network::Testnet | Network::Testnet4 => {
+                    Duration::from_secs(20)
+                }
                 Network::Signet => Duration::from_secs(10),
             },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rust/src/transaction_watcher.rs` around lines 60 - 64, Collapse the duplicate
match arms in the normal_wait_time match by combining Network::Bitcoin,
Network::Testnet and Network::Testnet4 into a single pattern (e.g.
Network::Bitcoin | Network::Testnet | Network::Testnet4 =>
Duration::from_secs(20)) and keep Network::Signet => Duration::from_secs(10);
update the match in the normal_wait_time assignment in transaction_watcher.rs so
it uses the combined pattern and Duration::from_secs references shown.

79-90: Prefer a concrete error type over Box<dyn std::error::Error>.

The rest of the crate uses eyre::Result / typed errors; Box<dyn std::error::Error> here is less idiomatic and has Send caveats when crossing await points. Since the only failing call is get_confirmed_transaction, eyre::Result<WatchResult> (or the underlying node::client::Error) would be cleaner and consistent with actor.rs.

♻️ Proposed change
-    async fn check_txn(&mut self) -> Result<WatchResult, Box<dyn std::error::Error>> {
+    async fn check_txn(&mut self) -> eyre::Result<WatchResult> {
         let Some(client) = self.client.clone() else {
             error!("transaction watcher client not initialized");
             return Ok(WatchResult::Continue);
         };
 
         let txn = client.get_confirmed_transaction(self.tx_id.clone()).await?;
         match txn {
             Some(txn) => Ok(WatchResult::Found(txn)),
             None => Ok(WatchResult::Continue),
         }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rust/src/transaction_watcher.rs` around lines 79 - 90, The function check_txn
currently returns Result<WatchResult, Box<dyn std::error::Error>> which is
non-idiomatic for this crate and can cause Send/cross-await issues; change its
signature to use eyre::Result<WatchResult> (or the concrete node::client::Error
if you prefer) and update the function to propagate errors with ? as before from
client.get_confirmed_transaction(self.tx_id.clone()).await; ensure you add the
appropriate use/import for eyre::Result and keep the same handling of the
None/Some match that returns WatchResult::Continue/Found so callers (and
actor.rs patterns) stay consistent.
rust/src/manager/wallet_manager/actor.rs (1)

944-962: Comment and early-return path.

Minor: the updated doc comment on line 953 ("wait 30 seconds…") is accurate only for the first queued txid in a batch; other txids may wait much less. Consider rephrasing to "batch a follow-up scan within the next 30 seconds, then remove watcher(s)."

Also, if update_block_id or the immediate perform_scan_for_single_tx_id on lines 948–951 returns Err, the txid is never enqueued and the watcher is never removed — the watcher will keep polling until it finds the txn again. That's pre-existing behavior, but worth a glance whether enqueueing before the immediate scan would be safer.

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

In `@rust/src/manager/wallet_manager/actor.rs` around lines 944 - 962, Rename the
doc comment in mark_transaction_found to say something like "batch a follow-up
scan within the next 30 seconds, then remove watcher(s)" to avoid implying every
tx waits 30s; and to avoid leaving a watcher enqueued on transient scan errors,
push the tx_id onto pending_follow_up_scans and arm the timer (use
pending_follow_up_scans.push(tx_id) and set
pending_scan_timer_armed/scan_timer.set_timeout_for_weak(self.addr.clone(),
Duration::from_secs(30))) before calling update_block_id() and
perform_scan_for_single_tx_id(tx_id), so that even if update_block_id or
perform_scan_for_single_tx_id returns Err the tx has been enqueued and the
watcher will be removed by the follow-up scan logic; ensure you still return Err
on failure of the immediate scan after enqueueing so existing error handling
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 `@rust/src/manager/wallet_manager/actor.rs`:
- Around line 166-182: The tick implementation on WalletActor shortens per-tx
follow-up delay and allows duplicate txids; to fix, (1) decide whether batched
timing is acceptable — if not, switch from a single scan_timer to per-tx
scheduling in mark_transaction_found (e.g., spawn a per-tx delay task or store
per-tx deadlines) so each txid gets ~30s from its mark, otherwise preserve
current batched behavior but document it; and (2) make pending_follow_up_scans a
HashSet<Txid> (replace Vec<Txid> with HashSet and update mark_transaction_found
to insert) and in tick use drain() or drain_filter to consume the set and call
addr.perform_scan_for_single_tx_id(tx_id) and addr.remove_watcher_for_txn(tx_id)
once per unique txid to eliminate duplicates (update types and the loop in tick
accordingly).

In `@rust/src/transaction_watcher.rs`:
- Around line 68-77: The start_watching() path can return early when
client_builder.build().await? fails leaving self.client None and never arming
the timer; modify start_watching (and/or the error path) so that when
client_builder.build() returns Err you still schedule a retry: catch the error
instead of using ? in start_watching, log it, set self.client = None if
necessary, and call self.timer.set_timeout_for_weak(self.addr.clone(),
retry_duration) (or re-send start_watching to self.addr) so the watcher will
retry; also update tick to detect self.client.is_none() and call
start_watching() to restart the connect attempt.

---

Nitpick comments:
In `@rust/src/manager/wallet_manager/actor.rs`:
- Around line 944-962: Rename the doc comment in mark_transaction_found to say
something like "batch a follow-up scan within the next 30 seconds, then remove
watcher(s)" to avoid implying every tx waits 30s; and to avoid leaving a watcher
enqueued on transient scan errors, push the tx_id onto pending_follow_up_scans
and arm the timer (use pending_follow_up_scans.push(tx_id) and set
pending_scan_timer_armed/scan_timer.set_timeout_for_weak(self.addr.clone(),
Duration::from_secs(30))) before calling update_block_id() and
perform_scan_for_single_tx_id(tx_id), so that even if update_block_id or
perform_scan_for_single_tx_id returns Err the tx has been enqueued and the
watcher will be removed by the follow-up scan logic; ensure you still return Err
on failure of the immediate scan after enqueueing so existing error handling
remains intact.

In `@rust/src/transaction_watcher.rs`:
- Around line 60-64: Collapse the duplicate match arms in the normal_wait_time
match by combining Network::Bitcoin, Network::Testnet and Network::Testnet4 into
a single pattern (e.g. Network::Bitcoin | Network::Testnet | Network::Testnet4
=> Duration::from_secs(20)) and keep Network::Signet => Duration::from_secs(10);
update the match in the normal_wait_time assignment in transaction_watcher.rs so
it uses the combined pattern and Duration::from_secs references shown.
- Around line 79-90: The function check_txn currently returns
Result<WatchResult, Box<dyn std::error::Error>> which is non-idiomatic for this
crate and can cause Send/cross-await issues; change its signature to use
eyre::Result<WatchResult> (or the concrete node::client::Error if you prefer)
and update the function to propagate errors with ? as before from
client.get_confirmed_transaction(self.tx_id.clone()).await; ensure you add the
appropriate use/import for eyre::Result and keep the same handling of the
None/Some match that returns WatchResult::Continue/Found so callers (and
actor.rs patterns) stay consistent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 618bb37e-8172-4431-8046-eb7826550201

📥 Commits

Reviewing files that changed from the base of the PR and between ba61cd2 and 6013ad1.

📒 Files selected for processing (2)
  • rust/src/manager/wallet_manager/actor.rs
  • rust/src/transaction_watcher.rs

Comment thread rust/src/manager/wallet_manager/actor.rs
Comment thread rust/src/manager/wallet_manager/actor.rs Outdated
Comment thread rust/src/transaction_watcher.rs Outdated
Copy link
Copy Markdown

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
rust/src/manager/wallet_manager/actor.rs (1)

947-967: ⚠️ Potential issue | 🟡 Minor

Watcher is leaked if pre-queue steps fail.

pending_scans.pending_txids.push(tx_id) (line 957) only runs after the two ? awaits on update_block_id() and perform_scan_for_single_tx_id() (lines 951, 954). If either returns Err, the function returns early, tx_id is never queued, and remove_watcher_for_txn is never sent — the TransactionWatcher for tx_id stays in self.transaction_watchers indefinitely (its own timer was cleared in Tick on Found, so it also does nothing). Consider enqueuing the txid (and arming the timer) before the fallible awaits, or scheduling the removal in a failure-tolerant way.

🛠️ Sketch
     pub async fn mark_transaction_found(&mut self, tx_id: Txid) -> ActorResult<()> {
         info!("marking transaction found: {tx_id}");
 
+        // Queue for follow-up scan and watcher removal even if the immediate scan fails,
+        // otherwise a failing scan leaves the watcher stuck in the HashMap forever.
+        self.pending_scans.pending_txids.push(tx_id);
+        if !self.pending_scans.timer_armed {
+            self.pending_scans.timer_armed = true;
+            self.pending_scans
+                .timer
+                .set_timeout_for_weak(self.addr.clone(), Duration::from_secs(30));
+        }
+
         // update the height
         self.update_block_id().await?;
 
         // update the height and perform sync scan which will update the transactions
         self.perform_scan_for_single_tx_id(tx_id).await?;
 
-        // wait 30 seconds, run follow-up scans, then remove watcher(s).
-        self.pending_scans.pending_txids.push(tx_id);
-
-        if !self.pending_scans.timer_armed {
-            self.pending_scans.timer_armed = true;
-            self.pending_scans
-                .timer
-                .set_timeout_for_weak(self.addr.clone(), Duration::from_secs(30));
-        }
-
         Produces::ok(())
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rust/src/manager/wallet_manager/actor.rs` around lines 947 - 967, The current
mark_transaction_found path can early-return on errors from update_block_id() or
perform_scan_for_single_tx_id(), leaking the TransactionWatcher because
pending_scans.pending_txids and timer_armed are only set after those fallible
awaits; to fix, ensure the tx_id is queued and the timer is armed before calling
update_block_id()/perform_scan_for_single_tx_id() (i.e., move the
pending_scans.pending_txids.push(tx_id) and timer arming logic earlier in
mark_transaction_found), or alternatively wrap the awaits and on any Err send
the remove_watcher_for_txn message (or otherwise schedule removal) so that if
update_block_id() or perform_scan_for_single_tx_id() fail the watcher is
removed; refer to mark_transaction_found, pending_scans.pending_txids,
pending_scans.timer_armed, update_block_id, perform_scan_for_single_tx_id,
remove_watcher_for_txn, Tick and TransactionWatcher when making the change.
🧹 Nitpick comments (1)
rust/src/manager/wallet_manager/actor.rs (1)

66-71: timer_armed is derivable from pending_txids.

The invariant "timer armed ⇔ pending_txids non-empty" holds: you only arm the timer when pushing a txid, and tick() drains the queue and clears timer_armed together. Dropping the bool and using pending_txids.is_empty() as the arm-check removes a redundant source of truth.

♻️ Sketch
 #[derive(Debug, Default)]
 struct PendingScanState {
     timer: Timer,
     pending_txids: Vec<Txid>,
-    timer_armed: bool,
 }
     async fn tick(&mut self) -> ActorResult<()> {
         if !self.pending_scans.timer.tick() {
             return Produces::ok(());
         }
-        self.pending_scans.timer_armed = false;
         for tx_id in std::mem::take(&mut self.pending_scans.pending_txids) {
-        self.pending_scans.pending_txids.push(tx_id);
-        if !self.pending_scans.timer_armed {
-            self.pending_scans.timer_armed = true;
+        let was_empty = self.pending_scans.pending_txids.is_empty();
+        self.pending_scans.pending_txids.push(tx_id);
+        if was_empty {
             self.pending_scans
                 .timer
                 .set_timeout_for_weak(self.addr.clone(), Duration::from_secs(30));
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rust/src/manager/wallet_manager/actor.rs` around lines 66 - 71,
PendingScanState contains a redundant timer_armed bool; remove the timer_armed
field and replace all uses with checks on pending_txids.is_empty() (or
!pending_txids.is_empty()) so the invariant "timer armed ⇔ pending_txids
non-empty" is enforced from a single source of truth. Specifically, remove
timer_armed from struct PendingScanState, update any code that sets or clears
timer_armed (e.g., the push/arm location that previously did `if !timer_armed {
arm timer; timer_armed = true }` should become `if pending_txids.is_empty() {
arm timer }` before pushing; and the tick() or drain logic that cleared
timer_armed should stop/clear the timer based on pending_txids becoming empty
after draining). Also update any Default/derive usages if necessary to reflect
the removed field and adjust tests/usages that referenced timer_armed to use
pending_txids.is_empty() instead.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@rust/src/manager/wallet_manager/actor.rs`:
- Around line 947-967: The current mark_transaction_found path can early-return
on errors from update_block_id() or perform_scan_for_single_tx_id(), leaking the
TransactionWatcher because pending_scans.pending_txids and timer_armed are only
set after those fallible awaits; to fix, ensure the tx_id is queued and the
timer is armed before calling update_block_id()/perform_scan_for_single_tx_id()
(i.e., move the pending_scans.pending_txids.push(tx_id) and timer arming logic
earlier in mark_transaction_found), or alternatively wrap the awaits and on any
Err send the remove_watcher_for_txn message (or otherwise schedule removal) so
that if update_block_id() or perform_scan_for_single_tx_id() fail the watcher is
removed; refer to mark_transaction_found, pending_scans.pending_txids,
pending_scans.timer_armed, update_block_id, perform_scan_for_single_tx_id,
remove_watcher_for_txn, Tick and TransactionWatcher when making the change.

---

Nitpick comments:
In `@rust/src/manager/wallet_manager/actor.rs`:
- Around line 66-71: PendingScanState contains a redundant timer_armed bool;
remove the timer_armed field and replace all uses with checks on
pending_txids.is_empty() (or !pending_txids.is_empty()) so the invariant "timer
armed ⇔ pending_txids non-empty" is enforced from a single source of truth.
Specifically, remove timer_armed from struct PendingScanState, update any code
that sets or clears timer_armed (e.g., the push/arm location that previously did
`if !timer_armed { arm timer; timer_armed = true }` should become `if
pending_txids.is_empty() { arm timer }` before pushing; and the tick() or drain
logic that cleared timer_armed should stop/clear the timer based on
pending_txids becoming empty after draining). Also update any Default/derive
usages if necessary to reflect the removed field and adjust tests/usages that
referenced timer_armed to use pending_txids.is_empty() instead.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 875d996c-ee1c-4ea7-b14e-591785df5daf

📥 Commits

Reviewing files that changed from the base of the PR and between 6013ad1 and 1fc6a2e.

📒 Files selected for processing (2)
  • rust/src/manager/wallet_manager/actor.rs
  • rust/src/transaction_watcher.rs

@praveenperera
Copy link
Copy Markdown
Contributor

hey @kartikangiras have you addressed all the coderabbit and greptile comments? if so just resolve them, if not address them or respond to them. thanks

Copy link
Copy Markdown

@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.

♻️ Duplicate comments (2)
rust/src/transaction_watcher.rs (1)

72-78: ⚠️ Potential issue | 🟠 Major

Keep the watcher retrying when client initialization fails.

client_builder.build().await? can return before the timer is armed, leaving this transaction with no follow-up retry. Also, if tick ever runs with client == None, it only schedules another check without rebuilding the client.

Retry client initialization from the timer path
     pub async fn start_watching(&mut self) -> ActorResult<()> {
         debug!("start_watching for txn {}", self.tx_id);
-        let client = Arc::new(self.client_builder.build().await?);
-        self.client = Some(client);
-
-        // Trigger the first check immediately.
-        self.timer.set_timeout_for_weak(self.addr.clone(), Duration::ZERO);
+        match self.client_builder.build().await {
+            Ok(client) => {
+                self.client = Some(Arc::new(client));
+                // Trigger the first check immediately.
+                self.timer.set_timeout_for_weak(self.addr.clone(), Duration::ZERO);
+            }
+            Err(error) => {
+                error!("failed to build transaction watcher client, retrying: {error:?}");
+                self.client = None;
+                self.timer.set_timeout_for_weak(self.addr.clone(), Duration::from_secs(30));
+            }
+        }

         Produces::ok(())
     }

     async fn check_txn(&mut self) -> Result<WatchResult, Box<dyn std::error::Error>> {
         let Some(client) = self.client.clone() else {
             error!("transaction watcher client not initialized");
             return Ok(WatchResult::Continue);
         };

And in tick:

         debug!("checking txn: {}", self.tx_id);
+
+        if self.client.is_none() {
+            return self.start_watching().await;
+        }

         match self.check_txn().await {

Also applies to: 83-87, 119-122

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

In `@rust/src/transaction_watcher.rs` around lines 72 - 78, The current
start_watching uses client_builder.build().await? which can early-return on
error and leave the watcher without a retry; change start_watching to attempt
client_builder.build().await inside a match/if let, and on Err schedule the
timer via timer.set_timeout_for_weak(self.addr.clone(), <retry_duration>) and
return Ok(()), and on Ok wrap in Arc and assign to self.client then arm the
immediate timer; likewise update tick so that when self.client.is_none() it
attempts to rebuild the client (using client_builder.build().await), sets
self.client on success, and on failure schedules another timer retry instead of
only scheduling a check—use the existing symbols start_watching,
client_builder.build(), self.client, timer.set_timeout_for_weak, and tick to
locate and implement the changes.
rust/src/manager/wallet_manager/actor.rs (1)

66-70: ⚠️ Potential issue | 🟡 Minor

Confirm batching semantics and dedupe queued txids.

This still batches all txids behind the first 30-second timer, so txids added later may wait much less than 30 seconds. Vec<Txid> also allows duplicate follow-up scans/removals for the same transaction. If batching is intended, consider documenting it and switching the queue to a set for idempotency.

Possible dedupe-only adjustment
-use ahash::HashMap;
+use ahash::{HashMap, HashSet};

 #[derive(Debug, Default)]
 struct PendingScanState {
     timer: Timer,
-    pending_txids: Vec<Txid>,
+    pending_txids: HashSet<Txid>,
     timer_armed: bool,
 }

-        for tx_id in std::mem::take(&mut self.pending_scans.pending_txids) {
+        for tx_id in std::mem::take(&mut self.pending_scans.pending_txids) {
             send!(self.addr.perform_scan_for_single_tx_id(tx_id));
             send!(self.addr.remove_watcher_for_txn(tx_id));
         }

-        self.pending_scans.pending_txids.push(tx_id);
+        self.pending_scans.pending_txids.insert(tx_id);

Also applies to: 182-194, 967-975

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

In `@rust/src/manager/wallet_manager/actor.rs` around lines 66 - 70,
PendingScanState currently stores pending_txids in a Vec<Txid> which allows
duplicates and gives inconsistent wait times because new txids are batched
behind the first timer; change pending_txids to a HashSet<Txid> (or BTreeSet) to
dedupe and make scans idempotent, update all code that inserts/iterates/drains
pending_txids (e.g., where PendingScanState::timer is armed/checked and where
entries are collected for scan/remove), and add a short doc comment on
PendingScanState (and the analogous structs/fields at the other occurrences)
clarifying the batching semantics and expected max-wait behavior so callers know
that txids are grouped behind the current timer rather than per-item delays.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@rust/src/manager/wallet_manager/actor.rs`:
- Around line 66-70: PendingScanState currently stores pending_txids in a
Vec<Txid> which allows duplicates and gives inconsistent wait times because new
txids are batched behind the first timer; change pending_txids to a
HashSet<Txid> (or BTreeSet) to dedupe and make scans idempotent, update all code
that inserts/iterates/drains pending_txids (e.g., where PendingScanState::timer
is armed/checked and where entries are collected for scan/remove), and add a
short doc comment on PendingScanState (and the analogous structs/fields at the
other occurrences) clarifying the batching semantics and expected max-wait
behavior so callers know that txids are grouped behind the current timer rather
than per-item delays.

In `@rust/src/transaction_watcher.rs`:
- Around line 72-78: The current start_watching uses
client_builder.build().await? which can early-return on error and leave the
watcher without a retry; change start_watching to attempt
client_builder.build().await inside a match/if let, and on Err schedule the
timer via timer.set_timeout_for_weak(self.addr.clone(), <retry_duration>) and
return Ok(()), and on Ok wrap in Arc and assign to self.client then arm the
immediate timer; likewise update tick so that when self.client.is_none() it
attempts to rebuild the client (using client_builder.build().await), sets
self.client on success, and on failure schedules another timer retry instead of
only scheduling a check—use the existing symbols start_watching,
client_builder.build(), self.client, timer.set_timeout_for_weak, and tick to
locate and implement the changes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7c2be288-cf08-48a5-a61b-892b09af8516

📥 Commits

Reviewing files that changed from the base of the PR and between 1fc6a2e and b6435d1.

📒 Files selected for processing (2)
  • rust/src/manager/wallet_manager/actor.rs
  • rust/src/transaction_watcher.rs

@kartikangiras kartikangiras force-pushed the ticktrait branch 4 times, most recently from 974d0a4 to f8618e4 Compare April 27, 2026 18:58
Copy link
Copy Markdown

@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

♻️ Duplicate comments (1)
rust/src/transaction_watcher.rs (1)

72-88: ⚠️ Potential issue | 🟠 Major

Watcher stays dormant if client_builder.build() keeps failing.

When build() fails, you arm the timer for 10 s and leave self.client = None. On the next tick, check_txn() short-circuits with WatchResult::Continue (lines 91–94) because self.client is None, then schedules normal_wait_time and returns. There is no path that retries client_builder.build(), so a transient connectivity error at the moment start_watching runs leaves this TransactionWatcher permanently silent for tx_id. The previous review flagged this; the timer was added but the rebuild path on tick was not.

🛠️ One option: rebuild on tick when client is missing
     async fn tick(&mut self) -> ActorResult<()> {
         if !self.timer.tick() {
             return Produces::ok(());
         }
 
+        if self.client.is_none() {
+            return self.start_watching().await;
+        }
+
         debug!("checking txn: {}", self.tx_id);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rust/src/transaction_watcher.rs` around lines 72 - 88, start_watching
currently gives up if client_builder.build() fails and leaves self.client =
None, but check_txn doesn't retry — fix by adding a rebuild attempt inside
check_txn when self.client.is_none(): call client_builder.build().await, on
success wrap in Arc and set self.client, on failure log the error and re-arm the
timer via timer.set_timeout_for_weak(self.addr.clone(), Duration::from_secs(10))
and return WatchResult::Continue so the watcher retries later; keep
start_watching's initial timeout behavior (Duration::ZERO) but ensure check_txn
handles the rebuild path and schedules normal_wait_time only when a client
exists.
🧹 Nitpick comments (1)
rust/src/transaction_watcher.rs (1)

90-101: Prefer a Send + Sync error type.

Result<_, Box<dyn std::error::Error>> is not Send + Sync, which can break compilation under #[async_trait] if anything in tick ever needs to hold this error across an await on a non-current-thread runtime, and it loses the structured error info you already have available. Consider eyre::Result<WatchResult> (already used elsewhere in this crate) or Box<dyn std::error::Error + Send + Sync>.

♻️ Suggested signature
-    async fn check_txn(&mut self) -> Result<WatchResult, Box<dyn std::error::Error>> {
+    async fn check_txn(&mut self) -> eyre::Result<WatchResult> {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rust/src/transaction_watcher.rs` around lines 90 - 101, The check_txn
function currently returns Result<WatchResult, Box<dyn std::error::Error>> which
is not Send+Sync; change its signature to use a Send+Sync-capable error type
(e.g., eyre::Result<WatchResult> or Result<WatchResult, Box<dyn
std::error::Error + Send + Sync>>) so the async context (and any callers like
tick) can hold errors across awaits; update the function signature for check_txn
and any callers that propagate its Result accordingly, and ensure you import
eyre or adjust the Box error bound where used.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@rust/src/manager/wallet_manager/actor.rs`:
- Around line 967-975: The comment above the pending_scans timer is misleading:
update it to state that the 30s timer is only for follow-up scans and that
watcher removal is performed eagerly by TransactionWatcher::tick (it calls
send!(self.wallet_actor.remove_watcher_for_txn(tx_id))), so by the time timer
fires the watcher is usually already gone; alternatively, if you prefer the
delayed-removal design, remove the immediate removal call inside
TransactionWatcher::tick and rely on
pending_scans.timer.set_timeout_for_weak(self.addr.clone(),
Duration::from_secs(30)) and the logic in wallet_actor.remove_watcher_for_txn to
perform removal—pick one approach and make the code/comment consistent (refer to
pending_scans, timer.set_timeout_for_weak, TransactionWatcher::tick, and
remove_watcher_for_txn).

In `@rust/src/transaction_watcher.rs`:
- Around line 113-131: The match arm handling Ok(WatchResult::Found(txn)) should
not send a redundant remove_watcher_for_txn; remove the
send!(self.wallet_actor.remove_watcher_for_txn(tx_id)) call, keep
send!(self.wallet_actor.mark_transaction_found(tx_id)), and instead of
self.timer.clear() call self.timer.set_inactive() so the watcher stays alive for
the wallet's delayed cleanup; locate this logic around check_txn(),
WatchResult::Found, mark_transaction_found, remove_watcher_for_txn, and
timer.clear()/set_inactive().

---

Duplicate comments:
In `@rust/src/transaction_watcher.rs`:
- Around line 72-88: start_watching currently gives up if client_builder.build()
fails and leaves self.client = None, but check_txn doesn't retry — fix by adding
a rebuild attempt inside check_txn when self.client.is_none(): call
client_builder.build().await, on success wrap in Arc and set self.client, on
failure log the error and re-arm the timer via
timer.set_timeout_for_weak(self.addr.clone(), Duration::from_secs(10)) and
return WatchResult::Continue so the watcher retries later; keep start_watching's
initial timeout behavior (Duration::ZERO) but ensure check_txn handles the
rebuild path and schedules normal_wait_time only when a client exists.

---

Nitpick comments:
In `@rust/src/transaction_watcher.rs`:
- Around line 90-101: The check_txn function currently returns
Result<WatchResult, Box<dyn std::error::Error>> which is not Send+Sync; change
its signature to use a Send+Sync-capable error type (e.g.,
eyre::Result<WatchResult> or Result<WatchResult, Box<dyn std::error::Error +
Send + Sync>>) so the async context (and any callers like tick) can hold errors
across awaits; update the function signature for check_txn and any callers that
propagate its Result accordingly, and ensure you import eyre or adjust the Box
error bound where used.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ba657d18-3556-4ec6-85ad-863b32b81d21

📥 Commits

Reviewing files that changed from the base of the PR and between b6435d1 and f8618e4.

📒 Files selected for processing (2)
  • rust/src/manager/wallet_manager/actor.rs
  • rust/src/transaction_watcher.rs

Comment thread rust/src/manager/wallet_manager/actor.rs
Comment thread rust/src/transaction_watcher.rs
Copy link
Copy Markdown

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@rust/src/transaction_watcher.rs`:
- Around line 72-88: The build retry never actually gets re-run because tick()
sees self.client.is_none() and then schedules the normal wait, never re-invoking
start_watching; update tick() so that when check_txn() returns Continue because
self.client.is_none(), it schedules the client-retry timeout (use the same
timer.set_timeout_for_weak(self.addr.clone(), Duration::from_secs(10)) semantics
or a configurable retry delay) instead of scheduling normal_wait_time(),
ensuring start_watching() will be invoked again to retry client_builder.build();
reference tick(), check_txn(), start_watching(), client_builder.build(),
timer.set_timeout_for_weak(), and normal_wait_time() when making the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 52a9926a-b988-4a8f-b857-6bf61af0c02b

📥 Commits

Reviewing files that changed from the base of the PR and between f8618e4 and 7e2b19c.

📒 Files selected for processing (2)
  • rust/src/manager/wallet_manager/actor.rs
  • rust/src/transaction_watcher.rs

Comment thread rust/src/transaction_watcher.rs
@kartikangiras
Copy link
Copy Markdown
Contributor Author

@praveenperera I have address all the inline review comments and from bots as well.

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.

Refactor actors to use act-zero's Tick trait instead of manual tokio::time::sleep

2 participants