Skip to content

feat(rebalancer): inflight-aware rebalancing system#7921

Merged
paulbalaji merged 11 commits intomainfrom
feat/inflight-tracking-rebalancer
Jan 29, 2026
Merged

feat(rebalancer): inflight-aware rebalancing system#7921
paulbalaji merged 11 commits intomainfrom
feat/inflight-tracking-rebalancer

Conversation

@Mo-Hussain
Copy link
Collaborator

@Mo-Hussain Mo-Hussain commented Jan 28, 2026

Summary

This PR implements an inflight-aware rebalancing system that tracks pending transfers and coordinates rebalancing decisions based on real-time state. The system prevents spurious rebalancing caused by state mismatches between the Monitor and Explorer.

Key Components

  • ActionTracker: Tracks pending user transfers and rebalance actions with Explorer integration for startup recovery
  • CollateralDeficitStrategy: Just-in-time rebalancing based on collateral needs from pending inbound transfers
  • CompositeStrategy: Chain multiple strategies together with coordination between them
  • Confirmed Block Sync: Query balances at confirmed blocks to sync Monitor state with Explorer indexing

PR Stack

This PR consolidates the following PRs:

  1. feat: implement ActionTracker for inflight-message-aware rebalancing #7688 - feat: implement ActionTracker for inflight-message-aware rebalancing

    • ActionTracker class for tracking inflight rebalancing actions
    • InMemoryStore for persisting action state
    • ExplorerClient enhancements for rebalance tracking
  2. feat(rebalancer): IStrategy inflight context and adapter #7698 - feat(rebalancer): IStrategy inflight context and adapter

    • InflightContext interface with pendingRebalances and pendingTransfers
    • InflightContextAdapter for converting between tracking and strategy contexts
  3. feat(rebalancer): BaseStrategy inflight-aware enhancements #7699 - feat(rebalancer): BaseStrategy inflight-aware enhancements

    • reserveCollateral() to prevent draining collateral needed for incoming transfers
    • getAvailableBalance() for balance minus reserved collateral
    • Optional bridges field for bridge address awareness
  4. feat(rebalancer): CollateralDeficitStrategy for just-in-time rebalancing #7700 - feat(rebalancer): CollateralDeficitStrategy for just-in-time rebalancing

    • New strategy responding to collateral needs from pending inbound transfers
    • Prefers transfer origin chains when filling deficits
  5. feat(rebalancer): CompositeStrategy and array config format #7701 - feat(rebalancer): CompositeStrategy and array config format

    • CompositeStrategy for chaining multiple strategies sequentially
    • Routes from earlier strategies passed as pendingRebalances to later strategies
    • Backwards-compatible array config format
  6. feat(rebalancer): rebalancer service integration for inflight messages #7731 - feat(rebalancer): rebalancer service integration for inflight messages

    • RebalanceExecutionResult with messageId and txHash tracking
    • End-to-end integration of ActionTracker with RebalancerService
  7. feat(rebalancer): query balances at confirmed block to sync with Explorer #7759 - feat(rebalancer): query balances at confirmed block to sync with Explorer

    • Query balances at latestBlock - reorgPeriod to sync with Explorer indexing
    • Wait for reorgPeriod confirmations after sending transactions
    • Prevents spurious rebalancing from state mismatches
  8. refactor(rebalancer): type safety, bug fixes, and comprehensive test coverage #7901 - refactor(rebalancer): type safety, bug fixes, and comprehensive test coverage

    • RebalancingRoute renamed to StrategyRoute with required bridge field
    • Bug fixes for failure metrics and missing Dispatch events
    • +41 new tests (174 total)

Breaking Changes

  • IRebalancer.rebalance() now returns RebalanceExecutionResult[] instead of void
  • IStrategy.getRebalancingRoutes() accepts optional inflightContext parameter
  • IStrategy now requires a name property
  • RebalancingRoute renamed to StrategyRoute with required bridge field
  • MonitorEvent now includes confirmedBlockTags for confirmed block queries

Test Plan

  • All 174 rebalancer tests passing
  • SDK builds and tests pass
  • Linting passes

Summary by CodeRabbit

  • New Features

    • ActionTracker with Explorer recovery and periodic sync; intent/action tracking for rebalances
    • CollateralDeficitStrategy for just-in-time rebalancing
    • CompositeStrategy to chain multiple strategies
    • Per-chain confirmed-block querying to align balance views
  • Breaking Changes

    • Strategy configuration now uses an array form
    • rebalance() returns per-route execution results
    • Routes now require bridge selection
  • Bug Fixes

    • Better failure metrics and handling of missing dispatches; reduced oscillation between strategies

✏️ Tip: You can customize this high-level summary in your review settings.

@socket-security
Copy link

socket-security bot commented Jan 28, 2026

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addednpm/​@​types/​uuid@​10.0.01001007081100

View full report

@Mo-Hussain Mo-Hussain self-assigned this Jan 28, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 28, 2026

📝 Walkthrough

Walkthrough

This PR adds inflight-message-aware rebalancing: ActionTracker, InflightContextAdapter, new tracking stores, CollateralDeficitStrategy, CompositeStrategy, inflight-aware BaseStrategy changes, Rebalancer now returns per-route results, and config/schema moves to array-based strategy composition.

Changes

Cohort / File(s) Summary
Changesets
.changeset/*.md
Documents new ActionTracker, CollateralDeficitStrategy, CompositeStrategy, inflight-aware base strategy, and breaking API/type changes.
Config & Types
typescript/rebalancer/src/config/*
types.ts, RebalancerConfig.ts, RebalancerConfig.test.ts
Strategy config now accepts arrays; added CollateralDeficit schema; helpers exported (getStrategyChainNames, getAllBridges, getStrategyChainConfig) and validation (CollateralDeficit first).
Core Rebalancer & Service
src/core/*
Rebalancer.ts, Rebalancer.test.ts, RebalancerService.ts, RebalancerService.test.ts
rebalance() returns RebalanceExecutionResult[]; execution/result aggregation added; RebalancerService wired to ActionTracker and InflightContextAdapter for intent/action lifecycle.
Removed wrappers & tests
src/core/*
WithInflightGuard.ts, WithSemaphore.ts and tests
Old guard/semaphore wrappers and related tests removed — inflight handling moved into ActionTracker/adapter flows.
Strategy surface
src/strategy/*
BaseStrategy.ts, WeightedStrategy.ts, MinAmountStrategy.ts, CollateralDeficitStrategy.ts, CompositeStrategy.ts and tests
BaseStrategy gains inflight-aware APIs (reserve/simulate/filter), strategies accept bridgeConfigs/tokens, new CollateralDeficitStrategy and CompositeStrategy added; strategies expose name and produce StrategyRoute (includes bridge).
Strategy factory & index
StrategyFactory.ts, strategy/index.ts, README.md
Factory accepts array of StrategyConfig, returns CompositeStrategy when multiple; index and README updated to export and document new strategies and types.
Tracking subsystem
src/tracking/*
ActionTracker.ts, IActionTracker.ts, InflightContextAdapter.ts, types.ts, store/*, tests
New ActionTracker (Explorer integration), generic IStore + InMemoryStore, InflightContextAdapter converting tracker state to InflightContext, tracking types and extensive tests.
Monitor & Metrics
Monitor.ts, metrics/*
Monitor refactored to explicit async handlers and confirmedBlockTags per-chain; new metrics and counters for intents/actions (rebalancerIntentsCreatedTotal, rebalancerActionsCreatedTotal).
Explorer & utils
utils/ExplorerClient.ts, utils/bridgeUtils.ts, utils/balanceUtils.ts
ExplorerClient adds inflight queries (user transfers/actions) and new message normalization; bridgeUtils removed bridgeIsWarp and logger param; minor balance utils logging change.
Context factory & infra
factories/RebalancerContextFactory.ts, infra/*, cli/*
createActionTracker added to factory; CLI/helm chain extraction now uses getStrategyChainNames; sample env YAML updated to composite strategies.
SDK adapters
typescript/sdk/src/token/adapters/*
Removed isWarp parameter from getRebalanceQuotes across adapter interface and Evm adapter.
Tests & helpers
test/helpers.ts, many new/updated tests
Test infrastructure extended for RebalanceRoute/RebalanceExecutionResult, mocks, and thousands of lines of new tests covering ActionTracker, strategies, RebalancerService, Rebalancer, stores, and adapters.
Public exports
src/index.ts
New public types/exports: RebalanceRoute, RebalanceExecutionResult, StrategyRoute/InflightContext, InflightContextAdapter, ActionTracker and tracking stores; removed WithInflightGuard/WithSemaphore exports.

Sequence Diagram(s)

sequenceDiagram
    participant Monitor
    participant RebalancerService
    participant ActionTracker
    participant Explorer
    participant Strategy
    participant Rebalancer

    Monitor->>RebalancerService: tokenInfo (with confirmedBlockTags)
    RebalancerService->>ActionTracker: syncActionTracker(confirmedBlockTags)
    ActionTracker->>Explorer: getInflightUserTransfers / getInflightRebalanceActions
    Explorer-->>ActionTracker: inflight messages
    ActionTracker->>ActionTracker: update stores (transfers/intents/actions)
    RebalancerService->>ActionTracker: getInflightContext()
    ActionTracker-->>RebalancerService: { pendingRebalances, pendingTransfers }
    RebalancerService->>Strategy: getRebalancingRoutes(balances, inflightContext)
    Strategy-->>RebalancerService: proposed StrategyRoute[]
    RebalancerService->>ActionTracker: createRebalanceIntent(...) for each route
    RebalancerService->>Rebalancer: rebalance(rebalanceRoutes with intentId)
    Rebalancer->>Rebalancer: prepare & execute txs
    Rebalancer-->>RebalancerService: RebalanceExecutionResult[]
    RebalancerService->>ActionTracker: createRebalanceAction / complete/ fail updates
Loading
sequenceDiagram
    participant CompositeStrategy
    participant StrategyA as CollateralDeficit
    participant StrategyB as MinAmount
    participant Context as InflightContext

    CompositeStrategy->>StrategyA: getRebalancingRoutes(balances, {pending..., proposed: []})
    StrategyA-->>CompositeStrategy: routesA (proposed)
    CompositeStrategy->>StrategyB: getRebalancingRoutes(balances, {pending..., proposed: routesA})
    StrategyB-->>CompositeStrategy: routesB
    CompositeStrategy-->>CompositeStrategy: return routesA + routesB
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested reviewers

  • yorhodes
  • ltyu
  • antigremlin
  • Xaroz
  • xeno097

Poem

A swamp of code got tidied, aye — trackers, routes and stores align. 🌿
Old guards walked off the boggy bridge, new intents and actions shine. ✨
Composite tunes and deficit drums march, proposals passed along the line. 🥁
Rebalancer now returns its tales, each route with truth to sign. 🧭
Roll up yer sleeves, give it a look — this patch’s layered like a fine onion. 🧅

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 26.47% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main feature—an inflight-aware rebalancing system—which is the primary focus of this substantial PR consolidating eight related PRs.
Description check ✅ Passed The description is comprehensive and well-structured, covering summary, key components, PR stack with links, breaking changes, and test plan. All major sections are present and substantively filled out.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/inflight-tracking-rebalancer

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
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: 8

🤖 Fix all issues with AI agents
In @.changeset/action-tracker-implementation.md:
- Line 5: The changeset used present tense; update the description to past tense
by changing verbs like "tracks", "provides", and "includes" to "tracked",
"provided", and "included" (and any other present-tense verbs) so the
ActionTracker, Transfer, RebalanceIntent, RebalanceAction, IStore, InMemoryStore
and ExplorerClient references are described in past tense and the body conforms
to the changeset guideline requiring past-tense descriptions.

In @.changeset/collateral-deficit-strategy.md:
- Line 5: The changeset used present-tense verbs for the strategy description;
update the description in .changeset/collateral-deficit-strategy.md to past
tense (e.g., change "detects" to "detected" and "proposes" to "proposed") while
keeping "Added CollateralDeficitStrategy" and "Modified reserveCollateral()"
as-is so the file consistently describes what changed.

In @.changeset/composite-strategy.md:
- Line 5: The changeset description used present tense; update it to past tense.
Edit the lines mentioning "are passed" -> "were passed", "now uses" -> "used",
and rephrase the schema sentence to past tense (e.g., "unified schema types by
making bridgeLockTime optional and adding a name property to IStrategy for
better logging"). Keep the rest of the wording consistent with past-tense
changeset style and preserve the same content (CompositeStrategy,
routing/pending rebalances, array config, bridgeLockTime, IStrategy name).

In @.changeset/inflight-aware-rebalancing-major.md:
- Around line 5-25: Update the changeset
.changeset/inflight-aware-rebalancing-major.md to use past-tense wording
throughout: change phrases like "now returns", "now includes", "was renamed",
and any present-tense bullets (e.g., IRebalancer.rebalance() now returns,
IStrategy.getRebalancingRoutes() accepts, IStrategy now requires,
RebalancingRoute renamed, MonitorEvent now includes) to past-tense equivalents
(e.g., "returned", "accepted", "required", "was renamed", "included") so the
file describes what changed rather than what will change.

In `@typescript/rebalancer/src/core/WithInflightGuard.ts`:
- Around line 40-57: The current bridge collection (using getStrategyChainNames
-> getStrategyChainConfig -> chainConfig?.bridge) only grabs a single bridge per
chain and misses overrides/secondary strategies; update the logic that builds
the bridges array (the variable named bridges) to iterate all strategy configs
and their per-route/override entries in this.config.strategyConfig, collect
every bridge referenced (including secondary/override bridges), deduplicate the
list, and then pass that deduped bridges array into
this.explorer.hasUndeliveredRebalance (keeping routersByDomain and txSender as
before) so inflight rebalances on all bridges are considered.

In `@typescript/rebalancer/src/strategy/CompositeStrategy.ts`:
- Around line 13-15: Update the doc comment on class CompositeStrategy to
reflect the actual behavior: change the phrase that currently says
"pendingRebalances" to "proposedRebalances" and adjust the sentence to state
that routes from earlier strategies are passed as proposedRebalances to later
strategies (mentioning CompositeStrategy and the method that
delegates/aggregates strategy outputs such as the strategy execution/composition
method). Ensure the comment wording matches the code path where earlier strategy
outputs are forwarded as proposedRebalances so readers won’t be confused about
parameter names.

In `@typescript/rebalancer/src/tracking/store/InMemoryStore.test.ts`:
- Around line 133-211: The tests use chai-as-promised assertions
(.to.be.rejectedWith and .to.not.be.rejected) but the plugin isn't registered;
import chaiAsPromised (e.g. import chaiAsPromised from 'chai-as-promised') and
call chai.use(chaiAsPromised) near the top of InMemoryStore.test.ts before any
tests run so the promise-based assertions in the update/delete tests work
correctly; ensure the chai and chaiAsPromised symbols are referenced exactly as
used by the other rebalancer tests.

In `@typescript/rebalancer/src/utils/ExplorerClient.ts`:
- Around line 222-232: The GraphQL selection in ExplorerClient that builds the
explorer messages response is missing the origin_tx_recipient field which
ExplorerMessage and normalizeExplorerMessage expect; update the GraphQL query
used in the ExplorerClient (the query block returning fields like msg_id,
origin_domain_id, origin_tx_hash, origin_tx_sender, etc.) to include
origin_tx_recipient so normalizeExplorerMessage receives a defined value and
downstream consumers get the expected field.
🧹 Nitpick comments (21)
typescript/cli/src/context/strategies/chain/chainResolver.ts (1)

141-147: Add a clearer guard for missing/legacy strategy config.

Right now a mis-shaped strategyConfig (old format or missing) will likely blow up inside getStrategyChainNames and the CLI error won’t be very friendly. Consider a quick guard so the user gets a clear message.

💡 Suggested tweak
-  const chains = getStrategyChainNames(rebalancerConfig.strategyConfig);
+  assert(
+    Array.isArray(rebalancerConfig.strategyConfig),
+    'Invalid rebalancer config: "strategyConfig" must be an array of strategies',
+  );
+  const chains = getStrategyChainNames(rebalancerConfig.strategyConfig);
typescript/rebalancer/src/core/WithSemaphore.test.ts (1)

20-25: Trim the as any casts in test routes/config.
These double-casts can hide API drift. Prefer typed fixtures (or satisfies RebalanceRoute) and a typed config builder so tests fail when the contract changes.

As per coding guidelines: Avoid unnecessary type casts (as assertions), especially as unknown as X double-casts in TypeScript.

Also applies to: 50-55, 72-79, 82-86, 99-104

typescript/rebalancer/src/tracking/InflightContextAdapter.test.ts (1)

16-28: Prefer typed stubs/fixtures over as any.
Consider using satisfies IActionTracker/satisfies MultiProvider with stubbed methods, and typed address constants for sender/recipient. It keeps the tests honest without losing Sinon flexibility.

As per coding guidelines: Use proper typing over any or type assertions in TypeScript.

Also applies to: 57-59, 119-121, 171-173, 183-185

typescript/rebalancer/src/core/WithInflightGuard.test.ts (1)

51-53: Use buildTestRebalanceRoute() instead of as any for RebalanceRoute objects. The test helper already exists and is imported in this file—using it with typed overrides (e.g., buildTestRebalanceRoute({ origin: 'ethereum', intentId: 'test-1' })) ensures the route shape matches the type and catches any breaking changes to the contract.

Lines affected: 51-53, 82-84, 112-114

Replace:

{ origin: 'ethereum', intentId: 'test-1' } as any

With:

buildTestRebalanceRoute({ origin: 'ethereum', intentId: 'test-1' })
.changeset/inflight-aware-base-strategy.md (1)

5-5: Changeset description should use past tense, not present.

The coding guidelines say to write these descriptions in past tense. Right now we've got "is extended", "extended", "updated" - those need a wee bit of adjustment to sound like they already happened, not like they're happening now.

✏️ Suggested fix
-BaseStrategy is extended with inflight-aware rebalancing capabilities and bridge configuration support. RebalancingRoute extended with optional bridge field for bridge selection. Added three protected methods: reserveCollateral() to prevent draining collateral needed for incoming transfers, simulatePendingRebalances() for optional balance simulation, and filterRebalances() to filter routes based on actual balance sufficiency. The getRebalancingRoutes() method updated to accept optional inflightContext and integrate the new methods. getCategorizedBalances() signature updated to accept optional pendingRebalances parameter. BaseStrategy, WeightedStrategy, and MinAmountStrategy constructors extended with optional bridges parameter (ChainMap<Address[]>) to store configured bridge addresses per chain.
+Extended BaseStrategy with inflight-aware rebalancing capabilities and bridge configuration support. Extended RebalancingRoute with optional bridge field for bridge selection. Added three protected methods: reserveCollateral() to prevent draining collateral needed for incoming transfers, simulatePendingRebalances() for optional balance simulation, and filterRebalances() to filter routes based on actual balance sufficiency. Updated the getRebalancingRoutes() method to accept optional inflightContext and integrate the new methods. Updated getCategorizedBalances() signature to accept optional pendingRebalances parameter. Extended BaseStrategy, WeightedStrategy, and MinAmountStrategy constructors with optional bridges parameter (ChainMap<Address[]>) to store configured bridge addresses per chain.

As per coding guidelines: "Write changeset descriptions in past tense describing what changed, not what will change."

typescript/rebalancer/src/utils/ExplorerClient.ts (3)

47-50: The normalizeHex function returns falsy values as-is, which could cause type issues.

When hex is null or undefined, this returns that value directly, but the return type is implicitly string. Not a swamp-sized problem since the caller handles the fields, but something to keep an eye on.

const normalizeHex = (hex: string): string => {
  if (!hex) return hex; // returns undefined/null but typed as string
  ...
};

246-259: Consider extracting the duplicated error handling into a helper method.

This error-response parsing logic appears three times in the file (here, lines 134-148, and 342-355). Could be nice and tidy with a wee private helper like parseErrorResponse(res: Response).


126-130: Fetch calls have no timeout configured.

These Explorer API requests could hang if the service is slow or unresponsive. Might want to consider adding an AbortController with a timeout to keep things from getting stuck in the swamp. Same applies to the other two fetch calls in this file.

typescript/rebalancer/src/strategy/StrategyFactory.test.ts (1)

99-106: Worth adding a test for CompositeStrategy creation in the factory tests.

Like an ogre with layers, this factory's got 'em—single strategy goes straight through, but multiple strategies wrap up in a CompositeStrategy. The tests here only check the single-strategy path, so adding one more test that passes a multi-element array would round out the coverage nicely. Not urgent, but it'd be good to have.

typescript/rebalancer/src/interfaces/IStrategy.ts (1)

16-25: Bridge the gap with a better type definition.
The filterByConfiguredBridges method casts to StrategyRoute just to grab the bridge property—feels like working around the type system. Since pendingRebalances sometimes has a bridge (like the comment says), might as well type it properly. The tests already do this same pattern, so it's a well-trodden path.

♻️ Type adjustment
+export type PendingRebalanceRoute = Route & { bridge?: Address };
+
 export type InflightContext = {
   /**
    * In-flight rebalances from ActionTracker.
    * Uses Route[] because recovered intents (from Explorer startup recovery)
    * don't have bridge information. Some routes may have bridge at runtime.
    */
-  pendingRebalances: Route[];
+  pendingRebalances: PendingRebalanceRoute[];
   pendingTransfers: Route[];
   /** Routes from earlier strategies - always have bridge */
   proposedRebalances?: StrategyRoute[];
 };
typescript/rebalancer/src/tracking/store/InMemoryStore.ts (1)

47-51: The as any cast here is like onion layers - it hides what's underneath.

The coding guidelines say to avoid unnecessary type casts and prefer type guards for runtime narrowing. Since Status is a generic parameter representing the entity's status field, consider adding a type constraint or helper to avoid the as any:

🔧 Suggested approach
-  async getByStatus(status: Status): Promise<T[]> {
-    return Array.from(this.data.values()).filter(
-      (entity) => (entity as any).status === status,
-    );
-  }
+  async getByStatus(status: Status): Promise<T[]> {
+    return Array.from(this.data.values()).filter(
+      (entity) => 'status' in entity && entity.status === status,
+    );
+  }

Alternatively, extend TrackedActionBase to include status: string if all tracked entities have a status field. As per coding guidelines: "Use proper typing over any or type assertions in TypeScript."

typescript/rebalancer/src/core/RebalancerService.test.ts (1)

185-273: The setupDaemonTest helper is a beast, but a useful one.

This consolidates the complex wiring needed for daemon mode tests. One thing though - line 216 uses a comma operator trick:

...(intentIndex++, {}),

This increments intentIndex as a side effect while spreading an empty object. It works, but it's the kind of clever that might confuse the next person reading it. Consider making the increment explicit:

🔧 More readable alternative
   (actionTracker.createRebalanceIntent as Sinon.SinonStub).callsFake(
-    async () => ({
-      id: options.intentIds?.[intentIndex] ?? `intent-${intentIndex + 1}`,
-      status: 'not_started' as const,
-      ...(intentIndex++, {}),
-    }),
+    async () => {
+      const id = options.intentIds?.[intentIndex] ?? `intent-${intentIndex + 1}`;
+      intentIndex++;
+      return {
+        id,
+        status: 'not_started' as const,
+      };
+    },
   );
typescript/rebalancer/src/strategy/StrategyFactory.ts (1)

113-125: Drop the override type cast; tighten the types instead.
The as ChainMap<Partial<...>> hides mismatches. Better to make StrategyConfig.override already typed as ChainMap<Partial<BridgeConfig>> (or a compatible type) and pass it through without casting.

♻️ Suggested tweak
-        override: config.override as ChainMap<
-          Partial<{ bridge: string; bridgeMinAcceptedAmount: string | number }>
-        >,
+        override: config.override,
As per coding guidelines: "Avoid unnecessary type casts (`as` assertions), especially `as unknown as X` double-casts in TypeScript."
typescript/rebalancer/src/strategy/CollateralDeficitStrategy.ts (1)

296-315: Replace the StrategyRoute cast with a safer shape.
Casting Route to StrategyRoute for bridge sidesteps typing. A Route plus optional bridge keeps it safe without assertions.

♻️ Suggested tweak
-  private filterByConfiguredBridges(pendingRebalances?: Route[]): Route[] {
+  private filterByConfiguredBridges(
+    pendingRebalances?: Array<Route & Partial<Pick<StrategyRoute, 'bridge'>>>,
+  ): Array<Route & Partial<Pick<StrategyRoute, 'bridge'>>> {
...
-    return pendingRebalances.filter((rebalance) => {
-      const bridge = (rebalance as StrategyRoute).bridge;
+    return pendingRebalances.filter((rebalance) => {
+      const bridge = rebalance.bridge;
As per coding guidelines: "Avoid unnecessary type casts (`as` assertions), especially `as unknown as X` double-casts in TypeScript."
typescript/rebalancer/src/config/types.ts (1)

251-256: Avoid type cast for override config access.

Now look here - the coding guidelines say we shouldn't be usin' type assertions like as { bridge?: string }. Instead, we could handle this more properly with the actual schema type or a type guard.

♻️ Suggested refactor
       if (chainConfig.override) {
         for (const overrideConfig of Object.values(chainConfig.override)) {
-          const override = overrideConfig as { bridge?: string };
-          if (override.bridge) {
-            bridges.add(override.bridge);
+          if ('bridge' in overrideConfig && typeof overrideConfig.bridge === 'string') {
+            bridges.add(overrideConfig.bridge);
           }
         }
       }

As per coding guidelines: "Avoid unnecessary type casts (as assertions)... Prefer type guards for runtime narrowing in TypeScript rather than type assertions."

typescript/rebalancer/src/strategy/MinAmountStrategy.ts (1)

156-164: Non-null assertion could mask initialization issues.

That ! on this.tokensByChainName! is a bit risky, like wanderin' into the woods without checkin' for pitchforks first. If tokensByChainName is ever undefined, this'll throw at runtime with a confusing error instead of a helpful one.

Consider whether the base class guarantees this is always set, or add defensive handling.

♻️ Suggested defensive check
 protected getTokenByChainName(chainName: string): Token {
-  const token = this.tokensByChainName![chainName];
+  if (!this.tokensByChainName) {
+    throw new Error('tokensByChainName not initialized');
+  }
+  const token = this.tokensByChainName[chainName];

   if (token === undefined) {
     throw new Error(`Token not found for chain ${chainName}`);
   }

   return token;
 }
typescript/rebalancer/src/monitor/Monitor.ts (1)

41-63: Type cast on reorgPeriod string could be avoided.

At line 49, there's a cast as EthJsonRpcBlockParameterTag. The coding guidelines suggest avoiding unnecessary type casts. Since you're checking typeof reorgPeriod === 'string', TypeScript should narrow the type, but the return type ConfirmedBlockTag might need adjustment.

♻️ Possible approach

If ConfirmedBlockTag already includes EthJsonRpcBlockParameterTag | number | undefined, the cast might be unnecessary. Otherwise, consider whether the type definitions could be aligned to avoid the cast.

       if (typeof reorgPeriod === 'string') {
-        return reorgPeriod as EthJsonRpcBlockParameterTag;
+        // reorgPeriod is already narrowed to string, which should match EthJsonRpcBlockParameterTag
+        return reorgPeriod;
       }

As per coding guidelines: "Avoid unnecessary type casts (as assertions)."

typescript/rebalancer/src/tracking/ActionTracker.ts (1)

518-539: Type cast on reorgPeriod could be avoided.

Similar to Monitor.ts, line 526 has as ConfirmedBlockTag. Consider aligning types to avoid the cast.

♻️ Suggested adjustment
       if (typeof reorgPeriod === 'string') {
-        return reorgPeriod as ConfirmedBlockTag;
+        return reorgPeriod;
       }

If the type narrowing doesn't work due to ConfirmedBlockTag definition, consider adjusting the type definition itself.

As per coding guidelines: "Avoid unnecessary type casts (as assertions)."

typescript/rebalancer/src/strategy/BaseStrategy.ts (1)

471-495: Consider logging when token is missing for minimum amount check.

When tokensByChainName exists but doesn't contain the origin chain's token, the minimum amount check is silently skipped. This might be intentional, but a debug log could help troubleshoot configuration issues.

🔧 Optional: Add debug logging for missing token
       if (this.tokensByChainName) {
         const token = this.tokensByChainName[route.origin];
         if (token) {
           const bridgeConfig = this.getBridgeConfigForRoute(
             route.origin,
             route.destination,
           );
           const minAmount = BigInt(
             toWei(bridgeConfig.bridgeMinAcceptedAmount, token.decimals),
           );
           if (route.amount < minAmount) {
             this.logger.info(
               {
                 context: this.constructor.name,
                 origin: route.origin,
                 destination: route.destination,
                 amount: route.amount.toString(),
                 minAmount: minAmount.toString(),
               },
               'Dropping route below bridgeMinAcceptedAmount',
             );
             return false;
           }
+        } else {
+          this.logger.debug(
+            {
+              context: this.constructor.name,
+              origin: route.origin,
+            },
+            'Token not found for origin chain, skipping minAmount check',
+          );
         }
       }
typescript/rebalancer/src/test/helpers.ts (2)

49-60: Consider deterministic intentId generation for test reproducibility.

Using Date.now() at line 53 could lead to non-deterministic test outputs. For most tests this is fine, but if any test relies on intentId comparison, it might get swampy.

🔧 Optional: Use counter-based ID
+let testRouteCounter = 0;
+
 export function buildTestRebalanceRoute(
   overrides: Partial<RebalanceRoute> = {},
 ): RebalanceRoute {
   return {
-    intentId: overrides.intentId ?? `test-route-${Date.now()}`,
+    intentId: overrides.intentId ?? `test-route-${++testRouteCounter}`,
     origin: 'ethereum',
     destination: 'arbitrum',
     amount: ethers.utils.parseEther('100').toBigInt(),
     bridge: TEST_ADDRESSES.bridge,
     ...overrides,
   };
 }

441-448: Double cast as any as RebalancerConfig hides type errors.

This cast at line 448 masks potential type mismatches in the config construction. If RebalancerConfig requires fields not being set here, you won't know until runtime. Like ignoring warning signs in a swamp - might be fine, might not be.

Per coding guidelines, prefer explicit object composition or Partial<> patterns over double casts.

♻️ Consider using Partial type
 export function buildTestConfig(
   overrides: Partial<RebalancerConfig> = {},
   chains: string[] = ['chain1'],
-): RebalancerConfig {
+): Partial<RebalancerConfig> & Pick<RebalancerConfig, 'warpRouteId' | 'strategyConfig'> {
   // ... existing logic ...
   
   return {
     warpRouteId: 'test-route',
     ...restOverrides,
     strategyConfig,
-  } as any as RebalancerConfig;
+  };
 }

Alternatively, ensure all required fields of RebalancerConfig are explicitly set with defaults.

@codecov
Copy link

codecov bot commented Jan 28, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 77.02%. Comparing base (cec4ff7) to head (cf3f55d).
⚠️ Report is 18 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #7921   +/-   ##
=======================================
  Coverage   77.02%   77.02%           
=======================================
  Files         117      117           
  Lines        2651     2651           
  Branches      244      244           
=======================================
  Hits         2042     2042           
  Misses        593      593           
  Partials       16       16           
Components Coverage Δ
core 87.80% <ø> (ø)
hooks 71.86% <ø> (ø)
isms 81.10% <ø> (ø)
token 86.67% <ø> (ø)
middlewares 84.98% <ø> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@Mo-Hussain
Copy link
Collaborator Author

@claude

@claude
Copy link
Contributor

claude bot commented Jan 28, 2026

Claude finished @Mo-Hussain's task in 1m 57s —— View job


PR Review: Inflight-Aware Rebalancing System

Summary

This PR introduces a comprehensive inflight-aware rebalancing system for Hyperlane's warp route rebalancer. The implementation adds ActionTracker for tracking pending transfers and rebalance actions, CollateralDeficitStrategy for JIT rebalancing, and CompositeStrategy for multi-strategy coordination. The changes are substantial (+8930/-816 lines across 62 files) and represent a major version bump with several breaking changes.


🎯 Architecture Review

✅ Strengths

  1. Well-structured tracking system: ActionTracker cleanly separates concerns with Transfer, RebalanceIntent, and RebalanceAction entities
  2. Robust recovery mechanism: Explorer integration for startup recovery ensures continuity after restarts
  3. Comprehensive test coverage: 15 test files with 174 total tests (41 new)
  4. Confirmed block sync: Querying balances at latestBlock - reorgPeriod prevents state mismatches with Explorer
  5. Type safety improvements: StrategyRoute with required bridge field prevents runtime errors

⚠️ Issues Identified


🔴 Critical Issues

1. Bridge Collection Incomplete in WithInflightGuard (typescript/rebalancer/src/core/WithInflightGuard.ts:40-57)

Problem: Only collects one bridge per chain, missing overrides and secondary strategies. In multi-strategy setups, inflight rebalances on non-primary bridges won't be detected.

Impact: Could allow duplicate rebalances through different bridges

Fix needed:

// Current - only gets first bridge per chain
const bridges = chains
  .map((chain) => {
    const chainConfig = getStrategyChainConfig(this.config.strategyConfig, chain);
    return chainConfig?.bridge;
  })
  .filter((bridge): bridge is string => bridge !== undefined);

// Should use getAllBridges helper instead
const bridges = getAllBridges(this.config.strategyConfig);

Fix this →


2. Missing origin_tx_recipient in GraphQL Query (typescript/rebalancer/src/utils/ExplorerClient.ts:222-232)

Problem: getInflightUserTransfers query doesn't fetch origin_tx_recipient, but ExplorerMessage type expects it and normalizeExplorerMessage tries to normalize it.

Impact: Field will be undefined in returned messages, potentially breaking downstream logic

Fix needed: Add origin_tx_recipient to the GraphQL selection at line 229

Fix this →


🟡 Type Safety Issues

3. as any Cast in InMemoryStore (typescript/rebalancer/src/tracking/store/InMemoryStore.ts:47-51)

Problem: getByStatus uses as any to access status field, circumventing type safety

Recommendation: Add type constraint or use type guard

async getByStatus(status: Status): Promise<T[]> {
  return Array.from(this.data.values()).filter(
    (entity) => 'status' in entity && entity.status === status,
  );
}

Fix this →


4. Type Cast in Bridge Filter (typescript/rebalancer/src/strategy/CollateralDeficitStrategy.ts:296-315)

Problem: Casts Route to StrategyRoute just to access optional bridge field

Recommendation: Define proper intersection type:

type PendingRebalanceRoute = Route & { bridge?: Address };

Apply same fix to:

  • typescript/rebalancer/src/interfaces/IStrategy.ts:16-25
  • typescript/rebalancer/src/config/types.ts:251-256

5. Doc Comment Mismatch (typescript/rebalancer/src/strategy/CompositeStrategy.ts:13-15)

Problem: Comment says "Routes from earlier strategies are passed as pendingRebalances" but code passes them as proposedRebalances

Fix: Update comment to say proposedRebalances


🟠 Testing Issues

6. Missing chai-as-promised Setup (typescript/rebalancer/src/tracking/store/InMemoryStore.test.ts:133-211)

Problem: Uses .to.be.rejectedWith() without registering chai-as-promised plugin

Fix needed:

import chaiAsPromised from 'chai-as-promised';
chai.use(chaiAsPromised);

Fix this →


📝 Changeset Style Issues

7. Use Past Tense in Changesets

All changeset files use present tense but should use past tense per repo guidelines:

  • .changeset/action-tracker-implementation.md:5 - "tracks" → "tracked", "provides" → "provided", "includes" → "included"
  • .changeset/collateral-deficit-strategy.md:5 - "detects" → "detected", "proposes" → "proposed"
  • .changeset/composite-strategy.md:5 - "are passed" → "were passed", "now uses" → "used"
  • .changeset/inflight-aware-rebalancing-major.md - multiple present-tense phrases need updating

Fix all changesets →


🔵 Minor Recommendations

  1. Unnecessary Type Casts in Tests - Multiple test files use as any for route fixtures. Use typed helpers like buildTestRebalanceRoute() instead

  2. Missing Composite Strategy Factory Test (typescript/rebalancer/src/strategy/StrategyFactory.test.ts:99-106) - Only tests single strategy; add test for multi-element array creating CompositeStrategy

  3. Non-deterministic Test IDs (typescript/rebalancer/src/test/helpers.ts:49-60) - buildTestRebalanceRoute uses Date.now() for intentId, could cause flaky tests. Consider counter-based IDs.


✅ What Works Well

  • ActionTracker lifecycle management - Clean state transitions and proper intent→action→completion flow
  • Explorer integration - Robust recovery with proper error handling
  • Confirmed block sync - Prevents spurious rebalancing from state mismatches
  • Configuration validation - CollateralDeficit-first ordering enforced via Zod
  • Test coverage - 174 tests is excellent for this complexity
  • Breaking change documentation - Clear upgrade path in major changeset

📊 Test Coverage

  • ✅ 15 test files with 174 total tests (41 new)
  • ✅ ActionTracker fully tested (startup recovery, sync operations, entity lifecycle)
  • ✅ Strategy tests updated for inflight context
  • ✅ RebalancerService daemon mode integration tested
  • ⚠️ Missing CompositeStrategy factory test

🎯 Recommendations

Before Merge:

  1. ✅ Fix WithInflightGuard bridge collection (critical)
  2. ✅ Add origin_tx_recipient to GraphQL query (critical)
  3. ✅ Fix changeset tenses (required by repo guidelines)
  4. ⚠️ Fix chai-as-promised setup (test reliability)

Nice to Have:

  • Remove as any casts in favor of proper types
  • Add CompositeStrategy factory test
  • Use deterministic test IDs

🔐 Security Considerations

  • ✅ No credential exposure in configs
  • ✅ Proper input validation via Zod schemas
  • ✅ Recovery mechanism prevents fund loss after restart
  • ✅ Inflight guard prevents duplicate rebalances (once bridge collection bug is fixed)

📦 Final Verdict

Overall Assessment: Well-architected system with comprehensive testing. The inflight-aware design properly addresses state synchronization issues between Monitor and Explorer. Two critical bugs need fixing before merge (bridge collection and GraphQL field), and changeset style updates are required per repo guidelines.

Recommendation: Approve with required changes - Fix the 2 critical issues and changeset style, then merge.


🔗 Related Context


@Mo-Hussain Mo-Hussain force-pushed the feat/inflight-tracking-rebalancer branch from e161cc7 to a99b3ce Compare January 28, 2026 12:57
Copy link
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: 0

Caution

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

⚠️ Outside diff range comments (1)
typescript/cli/src/tests/ethereum/warp/warp-rebalancer.e2e-test.ts (1)

446-502: Re‑enable or track the newly skipped e2e paths.

These skips mute coverage on key flows (inflight handling, happy‑path rebalance, metrics off, warp‑bridge). If they’re flaky, could we add a TODO + issue link and re‑enable soon, so the new behavior stays guarded?

Want me to draft a small TODO/issue template to track re‑enablement?

Also applies to: 970-1018, 1263-1278, 1332-1476, 1652-1813

Copy link
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

🤖 Fix all issues with AI agents
In `@typescript/rebalancer/src/core/RebalancerService.ts`:
- Around line 422-478: In executeWithTracking, guard against partial intent
creation by surrounding the loop that calls actionTracker.createRebalanceIntent
with try/catch: if createRebalanceIntent throws, call
actionTracker.failRebalanceIntent (or cancelRebalanceIntent if available) for
every intent id already pushed to intentIds to mark them failed, log any errors
from those cleanup calls, then rethrow or return to avoid leaving "not_started"
intents; apply this same safeguard before calling this.rebalancer.rebalance so
rebalanceRoutes and intentIds cannot be left half-populated.

In `@typescript/rebalancer/src/strategy/CollateralDeficitStrategy.ts`:
- Around line 179-182: getRebalancingRoutes calls getCategorizedBalances without
forwarding the composed routes, so simulated balances ignore earlier strategies'
proposed routes; update getRebalancingRoutes to pass
inflightContext?.proposedRebalances into getCategorizedBalances so the call
becomes getCategorizedBalances(effectiveBalances, pendingRebalances,
inflightContext?.proposedRebalances), ensuring getCategorizedBalances (and any
downstream balance simulation) includes routes from earlier strategies (refer to
symbols: getRebalancingRoutes, getCategorizedBalances, inflightContext,
proposedRebalances, pendingRebalances).
🧹 Nitpick comments (3)
typescript/rebalancer/src/tracking/store/InMemoryStore.ts (1)

31-40: Drop the as T cast in update.
You can keep type safety without the assertion by assigning to a typed variable.

♻️ Proposed tweak
-    this.data.set(id, {
-      ...existing,
-      ...updates,
-      updatedAt: Date.now(),
-    } as T);
+    const updated: T = {
+      ...existing,
+      ...updates,
+      updatedAt: Date.now(),
+    };
+    this.data.set(id, updated);

As per coding guidelines: Avoid unnecessary type casts (as assertions), especially as unknown as X double-casts in TypeScript. If types don't match, fix the underlying types rather than casting.

typescript/rebalancer/src/utils/ExplorerClient.ts (1)

46-64: Avoid any for Explorer rows; use a typed shape.
Keeps the normalization path safer and self-documenting.

♻️ Example direction
-  private normalizeExplorerMessage(msg: any): ExplorerMessage {
+  type ExplorerMessageRow = {
+    msg_id: string;
+    origin_domain_id: number;
+    destination_domain_id: number;
+    sender: string;
+    recipient: string;
+    origin_tx_hash: string;
+    origin_tx_sender: string;
+    origin_tx_recipient: string;
+    is_delivered: boolean;
+    message_body: string;
+  };
+
+  private normalizeExplorerMessage(msg: ExplorerMessageRow): ExplorerMessage {
     ...
   }

-    const validatedRows = rows.filter((msg: any) => {
+    const validatedRows = rows.filter((msg: ExplorerMessageRow) => {
       ...
     });

-    return messages.map((msg: any) => this.normalizeExplorerMessage(msg));
+    return messages.map((msg: ExplorerMessageRow) =>
+      this.normalizeExplorerMessage(msg),
+    );

As per coding guidelines: Use proper typing over any or type assertions in TypeScript.

Also applies to: 155-165, 262-265, 358-360

typescript/rebalancer/src/strategy/CollateralDeficitStrategy.ts (1)

383-389: Non-null assertion might bite ye later.

Line 384 uses this.tokensByChainName! with a non-null assertion. While the constructor passes tokensByChainName to the base class, if BaseStrategy stores it as optional and it somehow ends up undefined, this would throw at runtime without a helpful error message. The check on line 385-387 would never trigger.

Consider a defensive approach or verifying the base class guarantees this field is always set when passed to the constructor.

♻️ Defensive alternative
   protected getTokenByChainName(chainName: string): Token {
-    const token = this.tokensByChainName![chainName];
+    if (!this.tokensByChainName) {
+      throw new Error('tokensByChainName not initialized');
+    }
+    const token = this.tokensByChainName[chainName];
     if (token === undefined) {
       throw new Error(`Token not found for chain ${chainName}`);
     }
     return token;
   }

@hyper-gonk
Copy link
Contributor

hyper-gonk bot commented Jan 28, 2026

♻️ Rebalancer Docker Image Built Successfully

Image Tags:

gcr.io/abacus-labs-dev/hyperlane-rebalancer:pr-7921
gcr.io/abacus-labs-dev/hyperlane-rebalancer:c9a8c01-20260128-143153

Copy link
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

🤖 Fix all issues with AI agents
In `@typescript/rebalancer/src/factories/RebalancerContextFactory.ts`:
- Around line 238-245: The code currently calls
getStrategyChainNames(this.config.strategyConfig) then uses
multiProvider.getSigner(chainNames[0]) and signer.getAddress() to set
rebalancerAddress, implicitly assuming the same signer/address across all
chains; update the implementation to either (a) validate that the signer address
is identical across every chain in chainNames by iterating chainNames, calling
multiProvider.getSigner(chain) and await signer.getAddress() for each and
throwing an explicit error if any differ, or (b) add a clear comment near
getStrategyChainNames / multiProvider.getSigner that documents the single-signer
assumption and when it is acceptable; reference getStrategyChainNames,
this.config.strategyConfig, multiProvider.getSigner, signer.getAddress and
rebalancerAddress when making the change so reviewers can find the logic.
🧹 Nitpick comments (2)
typescript/rebalancer/src/factories/RebalancerContextFactory.ts (2)

249-254: Multiple tokens per domain would overwrite - likely fine, but heads up.

If warpCore.tokens ever contains multiple tokens for the same chain, the later one overwrites the earlier. For standard warp routes this shouldn't happen, but if it does, you'd have a sneaky bug lurkin' in the swamp.


276-284: Redundant logging: routerCount and domainCount are identical.

Both values are Object.keys(routersByDomain).length. Either remove one or rename to somethin' more descriptive if they're meant to be distinct in the future. Not a big deal, just a small duplication.

♻️ Proposed cleanup
     this.logger.debug(
       {
         warpRouteId: this.config.warpRouteId,
         routerCount: Object.keys(routersByDomain).length,
         bridgeCount: bridges.length,
-        domainCount: Object.keys(routersByDomain).length,
       },
       'ActionTracker created successfully',
     );

@Mo-Hussain Mo-Hussain force-pushed the feat/inflight-tracking-rebalancer branch from c9a8c01 to d0c6e95 Compare January 28, 2026 14:57
Copy link
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

🤖 Fix all issues with AI agents
In `@typescript/rebalancer/README.md`:
- Around line 116-117: Update the README description of the `strategy` field to
reflect actual behavior: state that `CompositeStrategy` executes all
sub-strategies sequentially (not stopping at the first), accumulates routes from
each, and passes routes produced by earlier strategies to later ones via the
`proposedRebalances` parameter so they can coordinate and augment existing
routes; mention that strategies are evaluated in order and their results are
combined rather than selecting only the first non-empty result.
🧹 Nitpick comments (4)
typescript/rebalancer/src/utils/ExplorerClient.ts (2)

156-165: Inline hex normalization duplicates the helper logic.

There's a perfectly good normalizeHex function inside normalizeExplorerMessage, but here we're doin' the same dance manually. It's like buildin' two outhouses when one would do.

Consider extractin' normalizeHex as a private class method and reusing it here:

♻️ Suggested refactor
+  private normalizeHex(hex: string): string {
+    if (!hex) return hex;
+    return hex.startsWith('\\x') ? '0x' + hex.slice(2) : hex;
+  }
+
   private normalizeExplorerMessage(msg: any): ExplorerMessage {
-    const normalizeHex = (hex: string): string => {
-      if (!hex) return hex;
-      return hex.startsWith('\\x') ? '0x' + hex.slice(2) : hex;
-    };
-
     return {
-      msg_id: normalizeHex(msg.msg_id),
+      msg_id: this.normalizeHex(msg.msg_id),
       // ... rest uses this.normalizeHex
     };
   }

Then in the validation block:

-      const normalizedMsgRouter = msg.origin_tx_recipient?.startsWith('\\x')
-        ? '0x' + msg.origin_tx_recipient.slice(2)
-        : msg.origin_tx_recipient;
+      const normalizedMsgRouter = this.normalizeHex(msg.origin_tx_recipient);

126-130: No timeout on fetch requests could cause hangin' issues.

If the Explorer's havin' a bad day and doesn't respond, these fetch calls will just sit there like a donkey waitin' for breakfast. For a daemon that polls regularly, this could gum up the works.

Consider adding an AbortController with a timeout:

♻️ Suggested pattern
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 30_000); // 30s timeout

try {
  const res = await fetch(this.baseUrl, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ query, variables }),
    signal: controller.signal,
  });
  // ... handle response
} finally {
  clearTimeout(timeoutId);
}
typescript/rebalancer/src/factories/RebalancerContextFactory.ts (1)

262-271: Comment numbering is off - step 5 got lost somewhere.

The comments go 1, 2, 3, 4... then jump to 6. Someone must've removed step 5 and forgot to renumber. It's a small thing, but it makes the code look like it lost count after a long day in the swamp.

✏️ Suggested fix
-    // 6. Create ActionTracker
+    // 5. Create ActionTracker
     const tracker = new ActionTracker(

And update the following comment:

-    // 7. Create InflightContextAdapter
+    // 6. Create InflightContextAdapter
     const adapter = new InflightContextAdapter(tracker, this.multiProvider);
typescript/rebalancer/src/core/RebalancerService.ts (1)

248-256: Manual rebalance generates an intentId but skips ActionTracker entirely.

The manual path fires up a random UUID for the route, but it never registers that intent with ActionTracker like the daemon mode does. The daemon's executeWithTracking creates intents first, then runs the rebalance—but manual just goes straight to rebalancing without that tracking step.

Since manual rebalances are one-off operations outside the strategy cycle, not tracking them makes sense. But the orphaned intentId means inflight context won't include manually triggered rebalances, which could be a blind spot if someone fires one off while the daemon's running.

Worth adding a comment explaining this is intentional, or at minimum ensuring any monitoring dashboards account for untracked manual operations.

@Mo-Hussain Mo-Hussain force-pushed the feat/inflight-tracking-rebalancer branch from 58e6038 to 48eea58 Compare January 28, 2026 15:48
Copy link
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

🤖 Fix all issues with AI agents
In
`@typescript/infra/config/environments/mainnet3/rebalancer/USDCSTAGE/eclipsemainnet-config.yaml`:
- Around line 3-15: Update the explanatory comment above rebalanceStrategy:
collateralDeficit to reference the correct Arbitrum standard bridge address;
specifically, change the mistaken address in the comment that mentions
arbitrum's standard bridge (currently shown as 0x33e94B6D...) to the correct
standard bridge 0x4c19c653a8419A475d9B6735511cB81C15b8d9b2 so the comment
accurately reflects the Arbitrum standard bridge used elsewhere (note the
collateralDeficit strategy still intentionally uses a different FAST bridge
address under chains.arbitrum).

In `@typescript/rebalancer/src/core/Rebalancer.ts`:
- Around line 489-497: The reorgPeriod schema is too permissive (currently
z.union([ZUint, z.string()])) causing the cast in multiProvider.sendTransaction;
update the chainMetadataTypes.ts schema for reorgPeriod to only allow a number
(ZUint) or one of the valid EthJsonRpcBlockParameterTag string literals (e.g.
"earliest", "latest", "pending", "safe", "finalized") so its type matches
sendTransaction’s expected number | EthJsonRpcBlockParameterTag, then remove the
cast at the call site (multiProvider.sendTransaction / reorgPeriod) so the value
types flow correctly.

In `@typescript/rebalancer/src/tracking/ActionTracker.ts`:
- Around line 569-575: recoverAction currently only prevents duplicates by
checking rebalanceActionStore.get(msg.msg_id), but actions created earlier may
have a different UUID id while sharing the same messageId, causing duplicate
recovery; update recoverAction to also query the store for an existing action
with messageId equal to msg.msg_id (e.g., call a new or existing
rebalanceActionStore.getByMessageId or use rebalanceActionStore.find/filter to
locate an action where action.messageId === msg.msg_id) and treat that as an
existing action (log and return) before proceeding with initialization; ensure
you reference recoverAction, rebalanceActionStore.get, and msg.msg_id when
adding the additional check.
🧹 Nitpick comments (18)
typescript/rebalancer/src/tracking/InflightContextAdapter.test.ts (1)

15-28: The mock setup works, but those as any casts are a wee bit swampy.

Per coding guidelines, type casts should be avoided where possible. For test mocks where you only need specific methods, consider using Partial<> patterns or creating a minimal interface that satisfies the required shape.

// Alternative approach using Pick for type safety
const actionTracker: Pick<IActionTracker, 'getActiveRebalanceIntents' | 'getInProgressTransfers'> = {
  getActiveRebalanceIntents: Sinon.stub(),
  getInProgressTransfers: Sinon.stub(),
};

That said, this is test code and the current approach is functional - just something to keep in mind for clarity.

typescript/rebalancer/src/core/RebalancerService.test.ts (2)

212-218: This increment pattern is a bit like finding a donkey in your swamp - unexpected.

The comma operator (intentIndex++, {}) increments the counter while returning an empty object for the spread. While clever, it's not immediately obvious what's happening. Consider a more explicit approach:

♻️ Suggested refactor for clarity
   (actionTracker.createRebalanceIntent as Sinon.SinonStub).callsFake(
-    async () => ({
-      id: options.intentIds?.[intentIndex] ?? `intent-${intentIndex + 1}`,
-      status: 'not_started' as const,
-      ...(intentIndex++, {}),
-    }),
+    async () => {
+      const id = options.intentIds?.[intentIndex] ?? `intent-${intentIndex + 1}`;
+      intentIndex++;
+      return {
+        id,
+        status: 'not_started' as const,
+      };
+    },
   );

52-66: Heavy as unknown as casting in the mock setup.

Per coding guidelines, double-casts like as unknown as MultiProvider should be avoided where possible. For test mocks, consider creating a type-safe mock builder or using Partial<MultiProvider> with the minimum required properties. That said, I get it - mocking complex SDK types is sometimes like draining a swamp.

typescript/rebalancer/src/utils/ExplorerClient.ts (1)

43-64: Define an ExplorerMessageRow type for raw explorer data.

The code uses any in three places when handling raw GraphQL rows: the normalizeExplorerMessage parameter, the filter callback, and the map callbacks. A dedicated type keeps things structured and safer—like layers in an ogre, each layer has its place.

♻️ Proposed refactor
+type ExplorerMessageRow = {
+  msg_id: string | null;
+  origin_domain_id: number;
+  destination_domain_id: number;
+  sender: string | null;
+  recipient: string | null;
+  origin_tx_hash: string | null;
+  origin_tx_sender: string | null;
+  origin_tx_recipient: string | null;
+  is_delivered: boolean;
+  message_body: string | null;
+};
+
-  private normalizeExplorerMessage(msg: any): ExplorerMessage {
+  private normalizeExplorerMessage(msg: ExplorerMessageRow): ExplorerMessage {
-    const validatedRows = rows.filter((msg: any) => {
+    const validatedRows = rows.filter((msg: ExplorerMessageRow) => {
-    return messages.map((msg: any) => this.normalizeExplorerMessage(msg));
+    return messages.map((msg: ExplorerMessageRow) =>
+      this.normalizeExplorerMessage(msg),
+    );
typescript/rebalancer/src/strategy/CollateralDeficitStrategy.test.ts (1)

104-115: Minor documentation nit on units terminology.

The comment says "1005000000 (wei)" but USDC has 6 decimals, not 18 like ETH. Callin' it "wei" is a bit misleadin' - might want to say "base units" or "smallest units" instead to keep things clear for folks who wander into this test file later.

📝 Suggested comment clarification
-      // chain1: deficit = |-5 USDC| + 1000 USDC = 1005 USDC = 1005000000 (wei)
+      // chain1: deficit = |-5 USDC| + 1000 USDC buffer = 1005 USDC = 1_005_000_000 (base units, 6 decimals)
typescript/rebalancer/src/strategy/MinAmountStrategy.test.ts (1)

569-639: Good catch on the zero-amount edge case, this is exactly what me swamp needs.

This test covers an important scenario where deficit scaling can produce zero-amount routes due to integer division. The test setup is solid - it creates conditions where totalSurplus=1 and totalDeficit=198 would scale each deficit to (99 * 1) / 198 = 0.

One small thing though - the test has a redundant assertion. Lines 626-631 and 635-638 both check that all routes have non-zero amounts. The second assertion is sufficient on its own.

🧅 Consider simplifying the assertions
-      // All routes should have non-zero amounts
-      // (Chai's greaterThan doesn't support BigInt, so use direct comparison)
-      for (const route of routes) {
-        expect(
-          route.amount > 0n,
-          `Route amount should be > 0, got ${route.amount}`,
-        ).to.be.true;
-      }
-
-      // The single token of surplus may produce one route (or none if both scale to 0)
-      // Either way, no zero-amount routes should exist
       expect(
         routes.every((r) => r.amount > 0n),
         'All routes must have non-zero amounts',
       ).to.be.true;
typescript/rebalancer/src/strategy/WeightedStrategy.test.ts (1)

423-429: Avoid the double-cast; define a minimal interface instead.

Now, I know it might seem like a quick solution, but this as unknown as Token double-cast goes against the grain of proper typing. Per the coding guidelines, we should fix the underlying types rather than forcing them through a cast like an ogre squeezing through a small doorway.

Consider defining a minimal mock interface that matches only what the tests need:

♻️ Suggested approach
-    function createMockToken(chainName: string, decimals = 18): Token {
-      return {
-        chainName,
-        decimals,
-        addressOrDenom: ethers.constants.AddressZero,
-      } as unknown as Token;
-    }
+    // Define minimal interface matching what WeightedStrategy actually uses
+    interface MockToken {
+      chainName: string;
+      decimals: number;
+      addressOrDenom: string;
+    }
+
+    function createMockToken(chainName: string, decimals = 18): MockToken {
+      return {
+        chainName,
+        decimals,
+        addressOrDenom: ethers.constants.AddressZero,
+      };
+    }

Then update tokensByChainName typing to ChainMap<MockToken> for these tests, or use Partial<Token> if the strategy accepts it.

As per coding guidelines: "Avoid unnecessary type casts (as assertions), especially as unknown as X double-casts in TypeScript."

typescript/rebalancer/src/strategy/StrategyFactory.ts (1)

113-129: Consider consolidating extractBridgeConfigs with the test helper.

There's a similar extractBridgeConfigs function in typescript/rebalancer/src/test/helpers.ts. While the implementations differ slightly (this one handles override field), it might be worth having a single source of truth to avoid the two driftin' apart over time.

Also, the type cast at line 122-125 could potentially be avoided if the StrategyConfig type properly defined the override field structure. Not a blocker, but somethin' to tidy up later.

typescript/rebalancer/src/strategy/MinAmountStrategy.ts (1)

156-164: Non-null assertion could bite you later.

That ! on this.tokensByChainName![chainName] assumes tokensByChainName is always defined. The subsequent check handles the case where the token itself is missing, but if tokensByChainName is undefined, you'll get a runtime error before that check runs.

Consider adding an early guard or making the type guarantee stronger.

💡 Optional defensive check
 protected getTokenByChainName(chainName: string): Token {
+  if (!this.tokensByChainName) {
+    throw new Error('tokensByChainName not initialized');
+  }
-  const token = this.tokensByChainName![chainName];
+  const token = this.tokensByChainName[chainName];

   if (token === undefined) {
     throw new Error(`Token not found for chain ${chainName}`);
   }

   return token;
 }
typescript/rebalancer/src/core/Rebalancer.ts (1)

533-559: Heads up on single Dispatch message assumption.

Line 556 takes dispatchedMessages[0].id without checking if there could be multiple. If a rebalance transaction somehow emits multiple Dispatch events (unlikely but possible with complex bridge interactions), you'd only capture the first one.

Might want to log a warning if dispatchedMessages.length > 1 just so you know when it happens.

💡 Add warning for multiple dispatches
   private buildResult(
     transaction: PreparedTransaction,
     receipt: providers.TransactionReceipt,
   ): RebalanceExecutionResult {
     const { origin, destination } = transaction.route;
     const dispatchedMessages = HyperlaneCore.getDispatchedMessages(receipt);

     if (dispatchedMessages.length === 0) {
       this.logger.error(
         { origin, destination, txHash: receipt.transactionHash },
         'No Dispatch event found in confirmed rebalance receipt',
       );
       return {
         route: transaction.route,
         success: false,
         error: `Transaction confirmed but no Dispatch event found`,
         txHash: receipt.transactionHash,
       };
     }

+    if (dispatchedMessages.length > 1) {
+      this.logger.warn(
+        {
+          origin,
+          destination,
+          txHash: receipt.transactionHash,
+          messageCount: dispatchedMessages.length,
+        },
+        'Multiple Dispatch events found, using first',
+      );
+    }
+
     return {
       route: transaction.route,
       success: true,
       messageId: dispatchedMessages[0].id,
       txHash: receipt.transactionHash,
     };
   }
typescript/rebalancer/src/config/types.ts (1)

251-258: Type cast in getAllBridges could be cleaner.

The as { bridge?: string } cast on line 253 works but isn't ideal per the coding guidelines about avoiding unnecessary type assertions. Since RebalancerBridgeConfigSchema.partial() is the type for override values, you could potentially narrow the type more explicitly.

💡 Type-safe alternative
       if (chainConfig.override) {
         for (const overrideConfig of Object.values(chainConfig.override)) {
-          const override = overrideConfig as { bridge?: string };
-          if (override.bridge) {
-            bridges.add(override.bridge);
+          if ('bridge' in overrideConfig && typeof overrideConfig.bridge === 'string') {
+            bridges.add(overrideConfig.bridge);
           }
         }
       }

Based on learnings: "Avoid unnecessary type casts (as assertions)... Prefer type guards for runtime narrowing in TypeScript rather than type assertions."

typescript/rebalancer/src/monitor/Monitor.ts (1)

41-63: Confirmed block calculation looks reasonable, but the cast could be cleaner.

The logic to compute latestBlock - reorgPeriod is solid. However, line 49's cast as EthJsonRpcBlockParameterTag assumes any string reorgPeriod is a valid block tag (like 'finalized' or 'safe'). If the metadata contains an unexpected string, this could pass through unchecked.

💡 Consider validating string values
       if (typeof reorgPeriod === 'string') {
+        // Validate it's a known block tag
+        const validTags: EthJsonRpcBlockParameterTag[] = ['latest', 'earliest', 'pending', 'safe', 'finalized'];
+        if (!validTags.includes(reorgPeriod as EthJsonRpcBlockParameterTag)) {
+          this.logger.warn(
+            { chain: chainName, reorgPeriod },
+            'Unknown reorgPeriod string, using latest',
+          );
+          return undefined;
+        }
         return reorgPeriod as EthJsonRpcBlockParameterTag;
       }
typescript/rebalancer/src/test/helpers.ts (6)

37-91: Consider a deterministic default intentId in fixtures.

Line 53 uses Date.now() which can make snapshots or golden tests jittery. Maybe default to a stable string/counter and let overrides opt into randomness.


95-105: Avoid the as unknown as TokenAmount double-cast.

The double-cast hides mismatched shapes and makes refactors brittle. Prefer constructing a TokenAmount via the SDK factory/constructor or a satisfies TokenAmount mock with the full required shape.

As per coding guidelines: Avoid unnecessary type casts (as assertions), especially as unknown as X double-casts in TypeScript.


186-247: Prefer a typed mock over as unknown as MultiProvider.

The double-cast bypasses type checks. Consider defining a dedicated mock type (or using satisfies MultiProvider) so missing methods are caught early.

As per coding guidelines: Use proper typing over any or type assertions in TypeScript.


301-321: Type safety slip with protocol: 'ethereum' as any.

If protocol has a union/enum type, prefer the exact value instead of any. This keeps metadata mocks aligned with real config.

As per coding guidelines: Avoid unnecessary type casts (as assertions), especially as unknown as X double-casts in TypeScript.


333-357: Avoid token as unknown as Token in the test context.

That cast can mask missing properties on the mock token. Consider shaping createMockToken to satisfy Token directly.

As per coding guidelines: Adjust interface definitions to be compatible rather than using type casts in TypeScript.


392-448: Reduce any in buildTestConfig to catch config drift.

The as any and as any as RebalancerConfig make it easy to miss breaking changes in strategy config. A typed builder (or satisfies RebalancerConfig) would give you stronger guarantees.

As per coding guidelines: Use proper typing over any or type assertions in TypeScript.

Copy link
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: 0

Caution

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

⚠️ Outside diff range comments (1)
typescript/cli/src/tests/ethereum/warp/warp-rebalancer.e2e-test.ts (1)

1075-1106: Environment variable cleanup missing.

Setting process.env.PROMETHEUS_PORT without cleanup could cause test pollution if test order changes. Same issue exists at line 1109-1110. Might want to store the original value and restore it, or delete the key after. Otherwise, tests could start fightin' like Donkey and me on a long journey.

🧹 Suggested fix
   it('should successfully log metrics tracking', async () => {
+    const originalPort = process.env.PROMETHEUS_PORT;
     process.env.PROMETHEUS_PORT = '9091';
 
-    writeYamlOrJson(REBALANCER_CONFIG_PATH, {
+    try {
+      writeYamlOrJson(REBALANCER_CONFIG_PATH, {
       // ... existing config ...
-    });
+      });
 
-    await startRebalancerAndExpectLog('Wallet balance updated for token', {
-      withMetrics: true,
-    });
+      await startRebalancerAndExpectLog('Wallet balance updated for token', {
+        withMetrics: true,
+      });
+    } finally {
+      if (originalPort === undefined) {
+        delete process.env.PROMETHEUS_PORT;
+      } else {
+        process.env.PROMETHEUS_PORT = originalPort;
+      }
+    }
   });
🧹 Nitpick comments (1)
typescript/cli/src/tests/ethereum/warp/warp-rebalancer.e2e-test.ts (1)

1115-1120: Stale comment - timeout value mismatch.

The comment says "10s" but the actual timeout is 30000ms (30 seconds). Small thing, but comments that lie are like signs pointin' to the wrong swamp.

📝 Suggested fix
-      const timeout = 30000; // 10s
+      const timeout = 30000; // 30s

Copy link
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

🤖 Fix all issues with AI agents
In `@typescript/rebalancer/src/core/RebalancerService.ts`:
- Around line 445-505: When creating intents in executeWithTracking (inside the
loop that calls actionTracker.createRebalanceIntent and pushes intent.id into
intentIds and rebalanceRoutes), call this.metrics?.recordIntentCreated(...)
immediately after obtaining intent.id so the metrics layer records each new
intent; then update processExecutionResults (the method invoked as await
this.processExecutionResults(results)) to call
this.metrics?.recordActionAttempt(...) for each RebalanceExecutionResult (use
the result.intentId and success/failure info and any relevant route/bridge info)
before or as you handle each result so action attempts are recorded. Ensure
calls use the existing metric method names recordIntentCreated and
recordActionAttempt and guard with this.metrics? to avoid null checks.
🧹 Nitpick comments (2)
typescript/rebalancer/src/metrics/Metrics.ts (1)

64-68: Consider handling server close errors.

The server.close() callback receives an optional error parameter that's currently being ignored. During shutdown, this might be fine, but ye might want to at least log it so problems don't get lost in the swamp.

🔧 Optional: Handle close errors
 close(): Promise<void> {
-  return new Promise((resolve) => {
-    this.server.close(() => resolve());
+  return new Promise((resolve, reject) => {
+    this.server.close((err) => {
+      if (err) {
+        reject(err);
+      } else {
+        resolve();
+      }
+    });
   });
 }
typescript/rebalancer/src/core/RebalancerService.ts (1)

511-551: Consider adding defensive check for actionTracker.

The non-null assertions (this.actionTracker!) on lines 518 and 538 are safe in the current flow since executeWithTracking checks for existence. But if this method ever gets called from elsewhere, it could lead to a null pointer situation. A wee guard at the top wouldn't hurt.

🔧 Optional: Add defensive check
 private async processExecutionResults(
   results: RebalanceExecutionResult[],
 ): Promise<void> {
+  if (!this.actionTracker) {
+    this.logger.warn('ActionTracker not available, skipping result processing');
+    return;
+  }
+
   for (const result of results) {
     const intentId = result.route.intentId;

     if (result.success && result.messageId) {
-      await this.actionTracker!.createRebalanceAction({
+      await this.actionTracker.createRebalanceAction({

@Mo-Hussain Mo-Hussain force-pushed the feat/inflight-tracking-rebalancer branch from 6cc23a7 to cf3f55d Compare January 28, 2026 17:53
@hyper-gonk
Copy link
Contributor

hyper-gonk bot commented Jan 28, 2026

🐳 Monorepo Docker Image Built Successfully

Service Tag
📦 monorepo cf3f55d-20260128-175419
Full image paths
gcr.io/abacus-labs-dev/hyperlane-monorepo:cf3f55d-20260128-175419

@hyper-gonk
Copy link
Contributor

hyper-gonk bot commented Jan 28, 2026

⚙️ Node Service Docker Images Built Successfully

Service Tag
🔑 key-funder cf3f55d-20260128-175422
🔍 offchain-lookup-server cf3f55d-20260128-175422
♻️ rebalancer cf3f55d-20260128-175422
🕵️ warp-monitor cf3f55d-20260128-175422
Full image paths
gcr.io/abacus-labs-dev/hyperlane-key-funder:cf3f55d-20260128-175422
gcr.io/abacus-labs-dev/hyperlane-offchain-lookup-server:cf3f55d-20260128-175422
gcr.io/abacus-labs-dev/hyperlane-rebalancer:cf3f55d-20260128-175422
gcr.io/abacus-labs-dev/hyperlane-warp-monitor:cf3f55d-20260128-175422

Copy link
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

🤖 Fix all issues with AI agents
In `@typescript/rebalancer/src/config/types.ts`:
- Around line 242-263: In getAllBridges, remove the unnecessary type assertion
on overrideConfig (the "as { bridge?: string }" cast) and access
overrideConfig.bridge directly; update the inner loop that iterates over
Object.values(chainConfig.override) to treat overrideConfig as its declared type
and call if (overrideConfig.bridge) { bridges.add(overrideConfig.bridge); } so
the code relies on the existing RebalancerBridgeConfigSchema.partial() typing
instead of casting.

In `@typescript/rebalancer/src/strategy/MinAmountStrategy.ts`:
- Around line 156-158: The getTokenByChainName method is using a non-null
assertion on tokensByChainName; replace the assertion with optional chaining
(access tokensByChainName?.[chainName] instead of tokensByChainName![chainName])
so the code preserves the optional type and allows the existing undefined/error
guard in getTokenByChainName to run normally; update the reference in that
method accordingly (method: getTokenByChainName, property: tokensByChainName).
🧹 Nitpick comments (7)
typescript/rebalancer/src/utils/ExplorerClient.ts (1)

134-148: Consider extracting the duplicated error handling into a helper.

The error extraction logic (try JSON, fall back to text, fall back to message) appears three times. A wee helper method would tidy things up.

♻️ Optional refactor
private async extractErrorDetails(res: Response): Promise<string> {
  try {
    const errorJson = await res.json();
    return JSON.stringify(errorJson);
  } catch (_e) {
    try {
      return await res.text();
    } catch (_textError) {
      return 'Unable to read response body';
    }
  }
}

Then replace each error block with:

if (!res.ok) {
  const errorDetails = await this.extractErrorDetails(res);
  throw new Error(`Explorer query failed: ${res.status} ${errorDetails}`);
}

Also applies to: 247-260, 343-356

typescript/rebalancer/src/test/helpers.ts (1)

95-106: Double cast in test helper.

The as unknown as TokenAmount pattern is used here because the mock only implements a subset of TokenAmount. In test helpers this is generally acceptable, but if you find yourself needing the full type more often, consider creating a proper mock class or using a library like sinon-stub-whole-object.

Based on learnings: Avoid unnecessary type casts (as assertions), especially as unknown as X double-casts in TypeScript.

typescript/rebalancer/src/strategy/WeightedStrategy.test.ts (1)

423-429: Avoid the as unknown as Token double-cast, it's not exactly the fairest approach in the land.

Per coding guidelines, double-casts should be avoided. This mock could use Partial<Token> or a proper mock/stub that satisfies the interface requirements instead of forcing the type through unknown.

🧅 Suggested fix
-    function createMockToken(chainName: string, decimals = 18): Token {
-      return {
-        chainName,
-        decimals,
-        addressOrDenom: ethers.constants.AddressZero,
-      } as unknown as Token;
-    }
+    function createMockToken(chainName: string, decimals = 18): Partial<Token> {
+      return {
+        chainName,
+        decimals,
+        addressOrDenom: ethers.constants.AddressZero,
+      };
+    }

Then update the tokensByChainName type to ChainMap<Partial<Token>> or adjust the strategy constructor to accept Partial<Token> for test scenarios.

As per coding guidelines: "Avoid unnecessary type casts (as assertions), especially as unknown as X double-casts in TypeScript."

typescript/rebalancer/src/factories/RebalancerContextFactory.ts (1)

276-284: Redundant logging fields - routerCount and domainCount are the same value.

Both routerCount and domainCount derive from Object.keys(routersByDomain).length. Consider removing one or renaming for clarity if they're meant to convey different concepts.

🧅 Optional cleanup
     this.logger.debug(
       {
         warpRouteId: this.config.warpRouteId,
-        routerCount: Object.keys(routersByDomain).length,
+        domainCount: Object.keys(routersByDomain).length,
         bridgeCount: bridges.length,
-        domainCount: Object.keys(routersByDomain).length,
       },
       'ActionTracker created successfully',
     );
typescript/rebalancer/src/strategy/StrategyFactory.ts (1)

113-129: The extractBridgeConfigs helper has a type assertion that could be tightened up.

The cast at line 122-124 handles the loose override typing from config. While not a double-cast, consider defining stronger types in the config schema to avoid this assertion, or add a runtime validation if the override structure could vary.

🧅 Potential improvement

If the override field's type in StrategyConfig could be more precisely typed upstream, this cast wouldn't be needed. Alternatively, add a type guard:

private static extractBridgeConfigs(
  strategyConfig: StrategyConfig,
): ChainMap<BridgeConfigWithOverride> {
  const bridgeConfigs: ChainMap<BridgeConfigWithOverride> = {};

  for (const [chain, config] of Object.entries(strategyConfig.chains)) {
    bridgeConfigs[chain] = {
      bridge: config.bridge,
      bridgeMinAcceptedAmount: config.bridgeMinAcceptedAmount ?? 0,
      override: config.override ?? {},
    };
  }

  return bridgeConfigs;
}

This assumes BridgeConfigWithOverride.override can accept {} as a default.

typescript/rebalancer/src/strategy/BaseStrategy.ts (2)

84-100: Consider documenting the expected subclass contract for simulation helpers.

The simulatePendingRebalances and simulateProposedRebalances methods are provided as protected helpers, but subclasses are expected to call them within their getCategorizedBalances implementation. This works fine but the implicit contract might catch someone off guard.

A wee note in the class-level or method docstrings about this pattern would help folks not wander into the swamp unprepared.


407-410: Minor type inconsistency between simulation methods and abstract signature.

simulateProposedRebalances takes Route[] but the abstract getCategorizedBalances signature uses StrategyRoute[] for proposedRebalances. While this works (StrategyRoute extends Route), it's a bit like having different size boots - one fits inside the other but it's not quite comfortable.

Consider aligning the types for clarity, or adding a comment explaining the intentional flexibility.

@paulbalaji paulbalaji merged commit c89997d into main Jan 29, 2026
127 checks passed
@github-project-automation github-project-automation bot moved this from In Review to Done in Hyperlane Tasks Jan 29, 2026
@paulbalaji paulbalaji deleted the feat/inflight-tracking-rebalancer branch January 29, 2026 12:50
nambrot added a commit that referenced this pull request Jan 30, 2026
- Fix inflight-guard test: CLI rebalancer now has inflight tracking (from
  PR #7921), so test assertions differentiate between SimpleRunner (expects
  over-rebalancing) and CLIRebalancerRunner (expects correct behavior)
- Batch enrollment: Use enrollRemoteRouters() for efficiency instead of
  sequential enrollRemoteRouter() calls
- Remove snapshot functionality: createSnapshot/restoreSnapshot were unused
  outside tests and added complexity without clear benefit
- Add CI matrix testing: rebalancer-sim tests now run in parallel matrix
  with Foundry/Anvil setup

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants