@hyperlane-xyz/rebalancer 0.1.2 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (223) hide show
  1. package/README.md +134 -14
  2. package/dist/config/RebalancerConfig.d.ts +2 -2
  3. package/dist/config/RebalancerConfig.d.ts.map +1 -1
  4. package/dist/config/RebalancerConfig.js +4 -3
  5. package/dist/config/RebalancerConfig.js.map +1 -1
  6. package/dist/config/RebalancerConfig.test.js +434 -163
  7. package/dist/config/RebalancerConfig.test.js.map +1 -1
  8. package/dist/config/types.d.ts +1650 -290
  9. package/dist/config/types.d.ts.map +1 -1
  10. package/dist/config/types.js +124 -46
  11. package/dist/config/types.js.map +1 -1
  12. package/dist/core/Rebalancer.d.ts +14 -7
  13. package/dist/core/Rebalancer.d.ts.map +1 -1
  14. package/dist/core/Rebalancer.js +168 -99
  15. package/dist/core/Rebalancer.js.map +1 -1
  16. package/dist/core/Rebalancer.test.d.ts +2 -0
  17. package/dist/core/Rebalancer.test.d.ts.map +1 -0
  18. package/dist/core/Rebalancer.test.js +391 -0
  19. package/dist/core/Rebalancer.test.js.map +1 -0
  20. package/dist/core/RebalancerService.d.ts +16 -2
  21. package/dist/core/RebalancerService.d.ts.map +1 -1
  22. package/dist/core/RebalancerService.js +164 -21
  23. package/dist/core/RebalancerService.js.map +1 -1
  24. package/dist/core/RebalancerService.test.d.ts +2 -0
  25. package/dist/core/RebalancerService.test.d.ts.map +1 -0
  26. package/dist/core/RebalancerService.test.js +809 -0
  27. package/dist/core/RebalancerService.test.js.map +1 -0
  28. package/dist/factories/RebalancerContextFactory.d.ts +11 -0
  29. package/dist/factories/RebalancerContextFactory.d.ts.map +1 -1
  30. package/dist/factories/RebalancerContextFactory.js +60 -13
  31. package/dist/factories/RebalancerContextFactory.js.map +1 -1
  32. package/dist/index.d.ts +6 -6
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +4 -3
  35. package/dist/index.js.map +1 -1
  36. package/dist/interfaces/IMonitor.d.ts +6 -8
  37. package/dist/interfaces/IMonitor.d.ts.map +1 -1
  38. package/dist/interfaces/IMonitor.js.map +1 -1
  39. package/dist/interfaces/IRebalancer.d.ts +20 -4
  40. package/dist/interfaces/IRebalancer.d.ts.map +1 -1
  41. package/dist/interfaces/IStrategy.d.ts +18 -2
  42. package/dist/interfaces/IStrategy.d.ts.map +1 -1
  43. package/dist/metrics/Metrics.d.ts +4 -2
  44. package/dist/metrics/Metrics.d.ts.map +1 -1
  45. package/dist/metrics/Metrics.js +21 -1
  46. package/dist/metrics/Metrics.js.map +1 -1
  47. package/dist/metrics/scripts/metrics.d.ts +2 -0
  48. package/dist/metrics/scripts/metrics.d.ts.map +1 -1
  49. package/dist/metrics/scripts/metrics.js +12 -0
  50. package/dist/metrics/scripts/metrics.js.map +1 -1
  51. package/dist/monitor/Monitor.d.ts +8 -3
  52. package/dist/monitor/Monitor.d.ts.map +1 -1
  53. package/dist/monitor/Monitor.js +75 -15
  54. package/dist/monitor/Monitor.js.map +1 -1
  55. package/dist/strategy/BaseStrategy.d.ts +51 -5
  56. package/dist/strategy/BaseStrategy.d.ts.map +1 -1
  57. package/dist/strategy/BaseStrategy.js +199 -19
  58. package/dist/strategy/BaseStrategy.js.map +1 -1
  59. package/dist/strategy/CollateralDeficitStrategy.d.ts +65 -0
  60. package/dist/strategy/CollateralDeficitStrategy.d.ts.map +1 -0
  61. package/dist/strategy/CollateralDeficitStrategy.js +245 -0
  62. package/dist/strategy/CollateralDeficitStrategy.js.map +1 -0
  63. package/dist/strategy/CollateralDeficitStrategy.test.d.ts +2 -0
  64. package/dist/strategy/CollateralDeficitStrategy.test.d.ts.map +1 -0
  65. package/dist/strategy/CollateralDeficitStrategy.test.js +364 -0
  66. package/dist/strategy/CollateralDeficitStrategy.test.js.map +1 -0
  67. package/dist/strategy/CompositeStrategy.d.ts +18 -0
  68. package/dist/strategy/CompositeStrategy.d.ts.map +1 -0
  69. package/dist/strategy/CompositeStrategy.js +63 -0
  70. package/dist/strategy/CompositeStrategy.js.map +1 -0
  71. package/dist/strategy/CompositeStrategy.test.d.ts +2 -0
  72. package/dist/strategy/CompositeStrategy.test.d.ts.map +1 -0
  73. package/dist/strategy/CompositeStrategy.test.js +265 -0
  74. package/dist/strategy/CompositeStrategy.test.js.map +1 -0
  75. package/dist/strategy/MinAmountStrategy.d.ts +12 -5
  76. package/dist/strategy/MinAmountStrategy.d.ts.map +1 -1
  77. package/dist/strategy/MinAmountStrategy.js +23 -14
  78. package/dist/strategy/MinAmountStrategy.js.map +1 -1
  79. package/dist/strategy/MinAmountStrategy.test.js +88 -20
  80. package/dist/strategy/MinAmountStrategy.test.js.map +1 -1
  81. package/dist/strategy/StrategyFactory.d.ts +15 -6
  82. package/dist/strategy/StrategyFactory.d.ts.map +1 -1
  83. package/dist/strategy/StrategyFactory.js +48 -10
  84. package/dist/strategy/StrategyFactory.js.map +1 -1
  85. package/dist/strategy/StrategyFactory.test.js +2 -2
  86. package/dist/strategy/StrategyFactory.test.js.map +1 -1
  87. package/dist/strategy/WeightedStrategy.d.ts +13 -4
  88. package/dist/strategy/WeightedStrategy.d.ts.map +1 -1
  89. package/dist/strategy/WeightedStrategy.js +18 -6
  90. package/dist/strategy/WeightedStrategy.js.map +1 -1
  91. package/dist/strategy/WeightedStrategy.test.js +108 -18
  92. package/dist/strategy/WeightedStrategy.test.js.map +1 -1
  93. package/dist/strategy/index.d.ts +2 -0
  94. package/dist/strategy/index.d.ts.map +1 -1
  95. package/dist/strategy/index.js +2 -0
  96. package/dist/strategy/index.js.map +1 -1
  97. package/dist/test/helpers.d.ts +93 -3
  98. package/dist/test/helpers.d.ts.map +1 -1
  99. package/dist/test/helpers.js +267 -10
  100. package/dist/test/helpers.js.map +1 -1
  101. package/dist/tracking/ActionTracker.d.ts +49 -0
  102. package/dist/tracking/ActionTracker.d.ts.map +1 -0
  103. package/dist/tracking/ActionTracker.js +422 -0
  104. package/dist/tracking/ActionTracker.js.map +1 -0
  105. package/dist/tracking/ActionTracker.test.d.ts +2 -0
  106. package/dist/tracking/ActionTracker.test.d.ts.map +1 -0
  107. package/dist/tracking/ActionTracker.test.js +637 -0
  108. package/dist/tracking/ActionTracker.test.js.map +1 -0
  109. package/dist/tracking/IActionTracker.d.ts +101 -0
  110. package/dist/tracking/IActionTracker.d.ts.map +1 -0
  111. package/dist/tracking/IActionTracker.js +2 -0
  112. package/dist/tracking/IActionTracker.js.map +1 -0
  113. package/dist/tracking/InflightContextAdapter.d.ts +18 -0
  114. package/dist/tracking/InflightContextAdapter.d.ts.map +1 -0
  115. package/dist/tracking/InflightContextAdapter.js +35 -0
  116. package/dist/tracking/InflightContextAdapter.js.map +1 -0
  117. package/dist/tracking/InflightContextAdapter.test.d.ts +2 -0
  118. package/dist/tracking/InflightContextAdapter.test.d.ts.map +1 -0
  119. package/dist/tracking/InflightContextAdapter.test.js +172 -0
  120. package/dist/tracking/InflightContextAdapter.test.js.map +1 -0
  121. package/dist/tracking/index.d.ts +7 -0
  122. package/dist/tracking/index.d.ts.map +1 -0
  123. package/dist/tracking/index.js +6 -0
  124. package/dist/tracking/index.js.map +1 -0
  125. package/dist/tracking/store/IStore.d.ts +41 -0
  126. package/dist/tracking/store/IStore.d.ts.map +1 -0
  127. package/dist/tracking/store/IStore.js +2 -0
  128. package/dist/tracking/store/IStore.js.map +1 -0
  129. package/dist/tracking/store/InMemoryStore.d.ts +21 -0
  130. package/dist/tracking/store/InMemoryStore.d.ts.map +1 -0
  131. package/dist/tracking/store/InMemoryStore.js +40 -0
  132. package/dist/tracking/store/InMemoryStore.js.map +1 -0
  133. package/dist/tracking/store/InMemoryStore.test.d.ts +2 -0
  134. package/dist/tracking/store/InMemoryStore.test.d.ts.map +1 -0
  135. package/dist/tracking/store/InMemoryStore.test.js +290 -0
  136. package/dist/tracking/store/InMemoryStore.test.js.map +1 -0
  137. package/dist/tracking/store/index.d.ts +3 -0
  138. package/dist/tracking/store/index.d.ts.map +1 -0
  139. package/dist/tracking/store/index.js +2 -0
  140. package/dist/tracking/store/index.js.map +1 -0
  141. package/dist/tracking/types.d.ts +43 -0
  142. package/dist/tracking/types.d.ts.map +1 -0
  143. package/dist/tracking/types.js +2 -0
  144. package/dist/tracking/types.js.map +1 -0
  145. package/dist/utils/ExplorerClient.d.ts +39 -1
  146. package/dist/utils/ExplorerClient.d.ts.map +1 -1
  147. package/dist/utils/ExplorerClient.js +205 -2
  148. package/dist/utils/ExplorerClient.js.map +1 -1
  149. package/dist/utils/balanceUtils.js +2 -2
  150. package/dist/utils/balanceUtils.js.map +1 -1
  151. package/dist/utils/balanceUtils.test.js +1 -0
  152. package/dist/utils/balanceUtils.test.js.map +1 -1
  153. package/dist/utils/bridgeUtils.d.ts +1 -3
  154. package/dist/utils/bridgeUtils.d.ts.map +1 -1
  155. package/dist/utils/bridgeUtils.js +1 -5
  156. package/dist/utils/bridgeUtils.js.map +1 -1
  157. package/dist/utils/bridgeUtils.test.js +3 -14
  158. package/dist/utils/bridgeUtils.test.js.map +1 -1
  159. package/package.json +11 -9
  160. package/src/config/RebalancerConfig.test.ts +459 -163
  161. package/src/config/RebalancerConfig.ts +5 -3
  162. package/src/config/types.ts +159 -52
  163. package/src/core/Rebalancer.test.ts +632 -0
  164. package/src/core/Rebalancer.ts +247 -157
  165. package/src/core/RebalancerService.test.ts +1144 -0
  166. package/src/core/RebalancerService.ts +245 -23
  167. package/src/factories/RebalancerContextFactory.ts +115 -14
  168. package/src/index.ts +16 -4
  169. package/src/interfaces/IMonitor.ts +15 -8
  170. package/src/interfaces/IRebalancer.ts +22 -4
  171. package/src/interfaces/IStrategy.ts +23 -2
  172. package/src/metrics/Metrics.ts +26 -5
  173. package/src/metrics/scripts/metrics.ts +14 -0
  174. package/src/monitor/Monitor.ts +109 -22
  175. package/src/strategy/BaseStrategy.ts +316 -26
  176. package/src/strategy/CollateralDeficitStrategy.test.ts +551 -0
  177. package/src/strategy/CollateralDeficitStrategy.ts +390 -0
  178. package/src/strategy/CompositeStrategy.test.ts +405 -0
  179. package/src/strategy/CompositeStrategy.ts +102 -0
  180. package/src/strategy/MinAmountStrategy.test.ts +189 -88
  181. package/src/strategy/MinAmountStrategy.ts +44 -13
  182. package/src/strategy/StrategyFactory.test.ts +2 -2
  183. package/src/strategy/StrategyFactory.ts +91 -8
  184. package/src/strategy/WeightedStrategy.test.ts +187 -72
  185. package/src/strategy/WeightedStrategy.ts +41 -7
  186. package/src/strategy/index.ts +2 -0
  187. package/src/test/helpers.ts +418 -14
  188. package/src/tracking/ActionTracker.test.ts +783 -0
  189. package/src/tracking/ActionTracker.ts +647 -0
  190. package/src/tracking/IActionTracker.ts +140 -0
  191. package/src/tracking/InflightContextAdapter.test.ts +203 -0
  192. package/src/tracking/InflightContextAdapter.ts +42 -0
  193. package/src/tracking/index.ts +36 -0
  194. package/src/tracking/store/IStore.ts +48 -0
  195. package/src/tracking/store/InMemoryStore.test.ts +338 -0
  196. package/src/tracking/store/InMemoryStore.ts +58 -0
  197. package/src/tracking/store/index.ts +2 -0
  198. package/src/tracking/types.ts +74 -0
  199. package/src/utils/ExplorerClient.ts +266 -3
  200. package/src/utils/balanceUtils.test.ts +1 -0
  201. package/src/utils/balanceUtils.ts +2 -2
  202. package/src/utils/bridgeUtils.test.ts +3 -15
  203. package/src/utils/bridgeUtils.ts +0 -10
  204. package/dist/core/WithInflightGuard.d.ts +0 -20
  205. package/dist/core/WithInflightGuard.d.ts.map +0 -1
  206. package/dist/core/WithInflightGuard.js +0 -47
  207. package/dist/core/WithInflightGuard.js.map +0 -1
  208. package/dist/core/WithInflightGuard.test.d.ts +0 -2
  209. package/dist/core/WithInflightGuard.test.d.ts.map +0 -1
  210. package/dist/core/WithInflightGuard.test.js +0 -64
  211. package/dist/core/WithInflightGuard.test.js.map +0 -1
  212. package/dist/core/WithSemaphore.d.ts +0 -22
  213. package/dist/core/WithSemaphore.d.ts.map +0 -1
  214. package/dist/core/WithSemaphore.js +0 -67
  215. package/dist/core/WithSemaphore.js.map +0 -1
  216. package/dist/core/WithSemaphore.test.d.ts +0 -2
  217. package/dist/core/WithSemaphore.test.d.ts.map +0 -1
  218. package/dist/core/WithSemaphore.test.js +0 -83
  219. package/dist/core/WithSemaphore.test.js.map +0 -1
  220. package/src/core/WithInflightGuard.test.ts +0 -131
  221. package/src/core/WithInflightGuard.ts +0 -67
  222. package/src/core/WithSemaphore.test.ts +0 -111
  223. package/src/core/WithSemaphore.ts +0 -92
@@ -22,13 +22,15 @@ import { ProtocolType, tryFn } from '@hyperlane-xyz/utils';
22
22
 
23
23
  import { type IMetrics } from '../interfaces/IMetrics.js';
24
24
  import { type MonitorEvent } from '../interfaces/IMonitor.js';
25
- import { type RebalancingRoute } from '../interfaces/IStrategy.js';
25
+ import { type StrategyRoute } from '../interfaces/IStrategy.js';
26
26
 
27
27
  import { type PriceGetter } from './PriceGetter.js';
28
28
  import {
29
29
  metricsRegister,
30
+ rebalancerActionsCreatedTotal,
30
31
  rebalancerExecutionAmount,
31
32
  rebalancerExecutionTotal,
33
+ rebalancerIntentsCreatedTotal,
32
34
  rebalancerPollingErrorsTotal,
33
35
  updateManagedLockboxBalanceMetrics,
34
36
  updateNativeWalletBalanceMetrics,
@@ -64,10 +66,7 @@ export class Metrics implements IMetrics {
64
66
  .inc();
65
67
  }
66
68
 
67
- recordRebalanceAmount(
68
- route: RebalancingRoute,
69
- originTokenAmount: TokenAmount,
70
- ) {
69
+ recordRebalanceAmount(route: StrategyRoute, originTokenAmount: TokenAmount) {
71
70
  rebalancerExecutionAmount
72
71
  .labels({
73
72
  warp_route_id: this.warpRouteId,
@@ -90,6 +89,28 @@ export class Metrics implements IMetrics {
90
89
  .inc();
91
90
  }
92
91
 
92
+ recordIntentCreated(route: StrategyRoute, strategy: string) {
93
+ rebalancerIntentsCreatedTotal
94
+ .labels({
95
+ warp_route_id: this.warpRouteId,
96
+ strategy,
97
+ origin: route.origin,
98
+ destination: route.destination,
99
+ })
100
+ .inc();
101
+ }
102
+
103
+ recordActionAttempt(route: StrategyRoute, succeeded: boolean) {
104
+ rebalancerActionsCreatedTotal
105
+ .labels({
106
+ warp_route_id: this.warpRouteId,
107
+ origin: route.origin,
108
+ destination: route.destination,
109
+ succeeded: String(succeeded),
110
+ })
111
+ .inc();
112
+ }
113
+
93
114
  async processToken({
94
115
  token,
95
116
  bridgedSupply,
@@ -42,6 +42,20 @@ export const rebalancerPollingErrorsTotal = new Counter({
42
42
  labelNames: ['warp_route_id'],
43
43
  });
44
44
 
45
+ export const rebalancerIntentsCreatedTotal = new Counter({
46
+ name: 'hyperlane_rebalancer_intents_created_total',
47
+ help: 'Total number of rebalancing intents (routes) created.',
48
+ registers: [metricsRegister],
49
+ labelNames: ['warp_route_id', 'strategy', 'origin', 'destination'],
50
+ });
51
+
52
+ export const rebalancerActionsCreatedTotal = new Counter({
53
+ name: 'hyperlane_rebalancer_actions_created_total',
54
+ help: 'Total number of rebalancing actions (transactions) attempted.',
55
+ registers: [metricsRegister],
56
+ labelNames: ['warp_route_id', 'origin', 'destination', 'succeeded'],
57
+ });
58
+
45
59
  /**
46
60
  * Updates token balance metrics for a warp route token.
47
61
  */
@@ -1,10 +1,15 @@
1
- import EventEmitter from 'events';
2
1
  import { type Logger } from 'pino';
3
2
 
4
- import type { Token, WarpCore } from '@hyperlane-xyz/sdk';
3
+ import {
4
+ EthJsonRpcBlockParameterTag,
5
+ type Token,
6
+ type WarpCore,
7
+ } from '@hyperlane-xyz/sdk';
5
8
  import { sleep } from '@hyperlane-xyz/utils';
6
9
 
7
10
  import {
11
+ type ConfirmedBlockTag,
12
+ type ConfirmedBlockTags,
8
13
  type IMonitor,
9
14
  type MonitorEvent,
10
15
  MonitorEventType,
@@ -14,9 +19,12 @@ import {
14
19
 
15
20
  /**
16
21
  * Simple monitor implementation that polls warp route collateral balances and emits them as MonitorEvent.
22
+ * Awaits the TokenInfo handler before starting the next cycle to prevent race conditions.
17
23
  */
18
24
  export class Monitor implements IMonitor {
19
- private readonly emitter = new EventEmitter();
25
+ private tokenInfoHandler?: (event: MonitorEvent) => void | Promise<void>;
26
+ private errorHandler?: (event: Error) => void;
27
+ private startHandler?: () => void;
20
28
  private isMonitorRunning = false;
21
29
  private resolveStop: (() => void) | null = null;
22
30
  private stopPromise: Promise<void> | null = null;
@@ -30,25 +38,69 @@ export class Monitor implements IMonitor {
30
38
  private readonly logger: Logger,
31
39
  ) {}
32
40
 
41
+ private async getConfirmedBlockTag(
42
+ chainName: string,
43
+ ): Promise<ConfirmedBlockTag> {
44
+ try {
45
+ const metadata = this.warpCore.multiProvider.getChainMetadata(chainName);
46
+ const reorgPeriod = metadata.blocks?.reorgPeriod ?? 32;
47
+
48
+ if (typeof reorgPeriod === 'string') {
49
+ return reorgPeriod as EthJsonRpcBlockParameterTag;
50
+ }
51
+
52
+ const provider =
53
+ this.warpCore.multiProvider.getEthersV5Provider(chainName);
54
+ const latestBlock = await provider.getBlockNumber();
55
+ return Math.max(0, latestBlock - reorgPeriod);
56
+ } catch (error) {
57
+ this.logger.warn(
58
+ { chain: chainName, error: (error as Error).message },
59
+ 'Failed to get confirmed block, using latest',
60
+ );
61
+ return undefined;
62
+ }
63
+ }
64
+
65
+ private async computeConfirmedBlockTags(): Promise<ConfirmedBlockTags> {
66
+ const blockTags: ConfirmedBlockTags = {};
67
+ const chains = new Set(this.warpCore.tokens.map((t) => t.chainName));
68
+
69
+ for (const chain of chains) {
70
+ blockTags[chain] = await this.getConfirmedBlockTag(chain);
71
+ }
72
+
73
+ return blockTags;
74
+ }
75
+
33
76
  // overloads from IMonitor
34
77
  on(
35
78
  eventName: MonitorEventType.TokenInfo,
36
- fn: (event: MonitorEvent) => void,
79
+ fn: (event: MonitorEvent) => void | Promise<void>,
37
80
  ): this;
38
81
  on(eventName: MonitorEventType.Error, fn: (event: Error) => void): this;
39
82
  on(eventName: MonitorEventType.Start, fn: () => void): this;
40
- on(eventName: string, fn: (...args: any[]) => void): this {
41
- this.emitter.on(eventName, fn);
83
+ on(eventName: string, fn: (...args: any[]) => void | Promise<void>): this {
84
+ switch (eventName) {
85
+ case MonitorEventType.TokenInfo:
86
+ this.tokenInfoHandler = fn as (
87
+ event: MonitorEvent,
88
+ ) => void | Promise<void>;
89
+ break;
90
+ case MonitorEventType.Error:
91
+ this.errorHandler = fn as (event: Error) => void;
92
+ break;
93
+ case MonitorEventType.Start:
94
+ this.startHandler = fn as () => void;
95
+ break;
96
+ }
42
97
  return this;
43
98
  }
44
99
 
45
100
  async start() {
46
101
  if (this.isMonitorRunning) {
47
102
  // Cannot start the same monitor multiple times
48
- this.emitter.emit(
49
- MonitorEventType.Error,
50
- new MonitorStartError('Monitor already running'),
51
- );
103
+ this.errorHandler?.(new MonitorStartError('Monitor already running'));
52
104
  return;
53
105
  }
54
106
 
@@ -58,13 +110,19 @@ export class Monitor implements IMonitor {
58
110
  { checkFrequency: this.checkFrequency },
59
111
  'Monitor started',
60
112
  );
61
- this.emitter.emit(MonitorEventType.Start);
113
+ this.startHandler?.();
62
114
 
63
115
  while (this.isMonitorRunning) {
116
+ const cycleStart = Date.now();
117
+
64
118
  try {
65
119
  this.logger.debug('Polling cycle started');
120
+
121
+ const confirmedBlockTags = await this.computeConfirmedBlockTags();
122
+
66
123
  const event: MonitorEvent = {
67
124
  tokensInfo: [],
125
+ confirmedBlockTags,
68
126
  };
69
127
 
70
128
  for (const token of this.warpCore.tokens) {
@@ -76,7 +134,11 @@ export class Monitor implements IMonitor {
76
134
  },
77
135
  'Checking token',
78
136
  );
79
- const bridgedSupply = await this.getTokenBridgedSupply(token);
137
+ const blockTag = confirmedBlockTags[token.chainName];
138
+ const bridgedSupply = await this.getTokenBridgedSupply(
139
+ token,
140
+ blockTag,
141
+ );
80
142
 
81
143
  event.tokensInfo.push({
82
144
  token,
@@ -84,12 +146,12 @@ export class Monitor implements IMonitor {
84
146
  });
85
147
  }
86
148
 
87
- // Emit the event warp routes info
88
- this.emitter.emit(MonitorEventType.TokenInfo, event);
149
+ if (this.tokenInfoHandler) {
150
+ await this.tokenInfoHandler(event);
151
+ }
89
152
  this.logger.debug('Polling cycle completed');
90
153
  } catch (error) {
91
- this.emitter.emit(
92
- MonitorEventType.Error,
154
+ this.errorHandler?.(
93
155
  new MonitorPollingError(
94
156
  `Error during monitor execution cycle: ${(error as Error).message}`,
95
157
  error as Error,
@@ -97,12 +159,16 @@ export class Monitor implements IMonitor {
97
159
  );
98
160
  }
99
161
 
100
- // Wait for the specified check frequency before the next iteration
101
- await sleep(this.checkFrequency);
162
+ // Smart sleep: only wait for remaining time after cycle completes
163
+ const elapsed = Date.now() - cycleStart;
164
+ const remaining = this.checkFrequency - elapsed;
165
+ if (remaining > 0) {
166
+ await sleep(remaining);
167
+ }
168
+ // If elapsed >= checkFrequency, start next cycle immediately
102
169
  }
103
170
  } catch (error) {
104
- this.emitter.emit(
105
- MonitorEventType.Error,
171
+ this.errorHandler?.(
106
172
  new MonitorStartError(
107
173
  `Error starting monitor: ${(error as Error).message}`,
108
174
  error as Error,
@@ -111,7 +177,9 @@ export class Monitor implements IMonitor {
111
177
  }
112
178
 
113
179
  // After the loop has been gracefully terminated, we can clean up.
114
- this.emitter.removeAllListeners();
180
+ this.tokenInfoHandler = undefined;
181
+ this.errorHandler = undefined;
182
+ this.startHandler = undefined;
115
183
  this.logger.info('Monitor stopped');
116
184
 
117
185
  // If stop() was called, resolve the promise to signal that we're done.
@@ -124,6 +192,7 @@ export class Monitor implements IMonitor {
124
192
 
125
193
  private async getTokenBridgedSupply(
126
194
  token: Token,
195
+ blockTag?: ConfirmedBlockTag,
127
196
  ): Promise<bigint | undefined> {
128
197
  if (!token.isHypToken()) {
129
198
  this.logger.warn(
@@ -138,7 +207,25 @@ export class Monitor implements IMonitor {
138
207
  }
139
208
 
140
209
  const adapter = token.getHypAdapter(this.warpCore.multiProvider);
141
- const bridgedSupply = await adapter.getBridgedSupply();
210
+ let bridgedSupply: bigint | undefined;
211
+
212
+ try {
213
+ bridgedSupply = await adapter.getBridgedSupply({ blockTag });
214
+ this.logger.debug(
215
+ { chain: token.chainName, blockTag },
216
+ 'Queried confirmed balance',
217
+ );
218
+ } catch (error) {
219
+ this.logger.warn(
220
+ {
221
+ chain: token.chainName,
222
+ blockTag,
223
+ error: (error as Error).message,
224
+ },
225
+ 'Historical block query failed, falling back to latest',
226
+ );
227
+ bridgedSupply = await adapter.getBridgedSupply();
228
+ }
142
229
 
143
230
  if (bridgedSupply === undefined) {
144
231
  this.logger.warn(