@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
@@ -1,67 +0,0 @@
1
- /**
2
- * Prevents frequent rebalancing operations while bridges complete.
3
- */
4
- export class WithSemaphore {
5
- config;
6
- rebalancer;
7
- // Timestamp until which rebalancing should be blocked
8
- waitUntil = 0;
9
- // Lock to prevent concurrent rebalance execution
10
- executing = false;
11
- logger;
12
- constructor(config, rebalancer, logger) {
13
- this.config = config;
14
- this.rebalancer = rebalancer;
15
- this.logger = logger.child({ class: WithSemaphore.name });
16
- }
17
- /**
18
- * Rebalance with timing control
19
- * @param routes - Routes to process
20
- */
21
- async rebalance(routes) {
22
- if (this.executing) {
23
- this.logger.info('Currently executing rebalance. Skipping.');
24
- return;
25
- }
26
- // No routes mean the system is balanced so we reset the timer to allow new rebalancing
27
- if (!routes.length) {
28
- this.logger.info('No routes to execute. Assuming rebalance is complete. Resetting semaphore timer.');
29
- this.waitUntil = 0;
30
- return;
31
- }
32
- // Skip if still in waiting period
33
- if (Date.now() < this.waitUntil) {
34
- this.logger.info('Still in waiting period. Skipping rebalance.');
35
- return;
36
- }
37
- // The wait period will be determined by the bridge with the highest wait tolerance
38
- const highestTolerance = this.getHighestLockTime(routes);
39
- try {
40
- // Execute rebalance
41
- this.executing = true;
42
- await this.rebalancer.rebalance(routes);
43
- }
44
- finally {
45
- this.executing = false;
46
- }
47
- // Set new waiting period
48
- this.waitUntil = Date.now() + highestTolerance;
49
- this.logger.info({
50
- highestTolerance,
51
- waitUntil: this.waitUntil,
52
- }, 'Rebalance semaphore locked');
53
- }
54
- getHighestLockTime(routes) {
55
- return routes.reduce((highest, route) => {
56
- const origin = this.config.strategyConfig.chains[route.origin];
57
- if (!origin) {
58
- this.logger.error({ route }, 'Chain not found in config. Skipping.');
59
- throw new Error(`Chain ${route.origin} not found in config`);
60
- }
61
- const bridgeLockTime = origin.bridgeLockTime;
62
- const overrideLockTime = origin.override?.[route.destination]?.bridgeLockTime ?? 0;
63
- return Math.max(highest, bridgeLockTime, overrideLockTime);
64
- }, 0);
65
- }
66
- }
67
- //# sourceMappingURL=WithSemaphore.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"WithSemaphore.js","sourceRoot":"","sources":["../../src/core/WithSemaphore.ts"],"names":[],"mappings":"AAMA;;GAEG;AACH,MAAM,OAAO,aAAa;IAQL;IACA;IARnB,sDAAsD;IAC9C,SAAS,GAAW,CAAC,CAAC;IAC9B,iDAAiD;IACzC,SAAS,GAAY,KAAK,CAAC;IAClB,MAAM,CAAS;IAEhC,YACmB,MAAwB,EACxB,UAAuB,EACxC,MAAc;QAFG,WAAM,GAAN,MAAM,CAAkB;QACxB,eAAU,GAAV,UAAU,CAAa;QAGxC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,aAAa,CAAC,IAAI,EAAE,CAAC,CAAC;IAC5D,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,SAAS,CAAC,MAA0B;QACxC,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YACnB,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,0CAA0C,CAAC,CAAC;YAE7D,OAAO;QACT,CAAC;QAED,uFAAuF;QACvF,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC;YACnB,IAAI,CAAC,MAAM,CAAC,IAAI,CACd,kFAAkF,CACnF,CAAC;YAEF,IAAI,CAAC,SAAS,GAAG,CAAC,CAAC;YACnB,OAAO;QACT,CAAC;QAED,kCAAkC;QAClC,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;YAChC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,8CAA8C,CAAC,CAAC;YAEjE,OAAO;QACT,CAAC;QAED,mFAAmF;QACnF,MAAM,gBAAgB,GAAG,IAAI,CAAC,kBAAkB,CAAC,MAAM,CAAC,CAAC;QAEzD,IAAI,CAAC;YACH,oBAAoB;YACpB,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;YACtB,MAAM,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;QAC1C,CAAC;gBAAS,CAAC;YACT,IAAI,CAAC,SAAS,GAAG,KAAK,CAAC;QACzB,CAAC;QAED,yBAAyB;QACzB,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,gBAAgB,CAAC;QAE/C,IAAI,CAAC,MAAM,CAAC,IAAI,CACd;YACE,gBAAgB;YAChB,SAAS,EAAE,IAAI,CAAC,SAAS;SAC1B,EACD,4BAA4B,CAC7B,CAAC;IACJ,CAAC;IAEO,kBAAkB,CAAC,MAA0B;QACnD,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,KAAK,EAAE,EAAE;YACtC,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,cAAc,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;YAE/D,IAAI,CAAC,MAAM,EAAE,CAAC;gBACZ,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,EAAE,sCAAsC,CAAC,CAAC;gBACrE,MAAM,IAAI,KAAK,CAAC,SAAS,KAAK,CAAC,MAAM,sBAAsB,CAAC,CAAC;YAC/D,CAAC;YAED,MAAM,cAAc,GAAG,MAAM,CAAC,cAAc,CAAC;YAC7C,MAAM,gBAAgB,GACpB,MAAM,CAAC,QAAQ,EAAE,CAAC,KAAK,CAAC,WAAW,CAAC,EAAE,cAAc,IAAI,CAAC,CAAC;YAE5D,OAAO,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,cAAc,EAAE,gBAAgB,CAAC,CAAC;QAC7D,CAAC,EAAE,CAAC,CAAC,CAAC;IACR,CAAC;CACF"}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=WithSemaphore.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"WithSemaphore.test.d.ts","sourceRoot":"","sources":["../../src/core/WithSemaphore.test.ts"],"names":[],"mappings":""}
@@ -1,83 +0,0 @@
1
- import chai, { expect } from 'chai';
2
- import chaiAsPromised from 'chai-as-promised';
3
- import { pino } from 'pino';
4
- import Sinon from 'sinon';
5
- import { RebalancerStrategyOptions } from '../config/types.js';
6
- import { MockRebalancer, buildTestConfig } from '../test/helpers.js';
7
- import { WithSemaphore } from './WithSemaphore.js';
8
- chai.use(chaiAsPromised);
9
- const testLogger = pino({ level: 'silent' });
10
- describe('WithSemaphore', () => {
11
- it('should call the underlying rebalancer', async () => {
12
- const config = buildTestConfig();
13
- const routes = [
14
- {
15
- origin: 'chain1',
16
- },
17
- ];
18
- const rebalancer = new MockRebalancer();
19
- const rebalanceSpy = Sinon.spy(rebalancer, 'rebalance');
20
- const withSemaphore = new WithSemaphore(config, rebalancer, testLogger);
21
- await withSemaphore.rebalance(routes);
22
- expect(rebalanceSpy.calledOnce).to.be.true;
23
- expect(rebalanceSpy.calledWith(routes)).to.be.true;
24
- });
25
- it('should return early if there are no routes', async () => {
26
- const config = buildTestConfig();
27
- const rebalancer = new MockRebalancer();
28
- const rebalanceSpy = Sinon.spy(rebalancer, 'rebalance');
29
- const withSemaphore = new WithSemaphore(config, rebalancer, testLogger);
30
- await withSemaphore.rebalance([]);
31
- expect(rebalanceSpy.calledOnce).to.be.false;
32
- });
33
- it('should return early if rebalance occurs before waitUntil is reached', async () => {
34
- const config = buildTestConfig();
35
- const routes = [
36
- {
37
- origin: 'chain1',
38
- },
39
- ];
40
- const rebalancer = new MockRebalancer();
41
- const rebalanceSpy = Sinon.spy(rebalancer, 'rebalance');
42
- const withSemaphore = new WithSemaphore(config, rebalancer, testLogger);
43
- await withSemaphore.rebalance(routes);
44
- expect(rebalanceSpy.calledOnce).to.be.true;
45
- expect(rebalanceSpy.calledWith(routes)).to.be.true;
46
- rebalanceSpy.resetHistory();
47
- await withSemaphore.rebalance(routes);
48
- expect(rebalanceSpy.calledOnce).to.be.false;
49
- });
50
- it('should throw if a chain is missing', async () => {
51
- const config = buildTestConfig({
52
- strategyConfig: {
53
- rebalanceStrategy: RebalancerStrategyOptions.Weighted,
54
- chains: {},
55
- },
56
- });
57
- const routes = [
58
- {
59
- origin: 'chain1',
60
- },
61
- ];
62
- const rebalancer = new MockRebalancer();
63
- const withSemaphore = new WithSemaphore(config, rebalancer, testLogger);
64
- await expect(withSemaphore.rebalance(routes)).to.be.rejectedWith(`Chain ${routes[0].origin} not found in config`);
65
- });
66
- it('should not execute if another rebalance is currently executing', async () => {
67
- const config = buildTestConfig();
68
- const routes = [
69
- {
70
- origin: 'chain1',
71
- },
72
- ];
73
- const rebalancer = new MockRebalancer();
74
- const rebalanceSpy = Sinon.spy(rebalancer, 'rebalance');
75
- const withSemaphore = new WithSemaphore(config, rebalancer, testLogger);
76
- const rebalancePromise1 = withSemaphore.rebalance(routes);
77
- const rebalancePromise2 = withSemaphore.rebalance(routes);
78
- await rebalancePromise1;
79
- await rebalancePromise2;
80
- expect(rebalanceSpy.calledOnce).to.be.true;
81
- });
82
- });
83
- //# sourceMappingURL=WithSemaphore.test.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"WithSemaphore.test.js","sourceRoot":"","sources":["../../src/core/WithSemaphore.test.ts"],"names":[],"mappings":"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"}
@@ -1,131 +0,0 @@
1
- import chai, { expect } from 'chai';
2
- import chaiAsPromised from 'chai-as-promised';
3
- import { ethers } from 'ethers';
4
- import { pino } from 'pino';
5
- import Sinon from 'sinon';
6
-
7
- import { chainMetadata } from '@hyperlane-xyz/registry';
8
- import { ChainMetadataManager } from '@hyperlane-xyz/sdk';
9
-
10
- import { type RebalancingRoute } from '../interfaces/IStrategy.js';
11
- import { MockRebalancer, buildTestConfig } from '../test/helpers.js';
12
- import { ExplorerClient } from '../utils/ExplorerClient.js';
13
-
14
- import { WithInflightGuard } from './WithInflightGuard.js';
15
-
16
- chai.use(chaiAsPromised);
17
-
18
- const testLogger = pino({ level: 'silent' });
19
-
20
- describe('WithInflightGuard', () => {
21
- it('forwards empty routes without calling Explorer', async () => {
22
- const config = buildTestConfig();
23
-
24
- const rebalancer = new MockRebalancer();
25
- const rebalanceSpy = Sinon.spy(rebalancer, 'rebalance');
26
-
27
- const explorer = new ExplorerClient('http://localhost');
28
- const explorerSpy = Sinon.stub(explorer, 'hasUndeliveredRebalance');
29
-
30
- const guard = new WithInflightGuard(
31
- config,
32
- rebalancer,
33
- explorer,
34
- ethers.Wallet.createRandom().address,
35
- new ChainMetadataManager(chainMetadata as any),
36
- testLogger,
37
- );
38
-
39
- await guard.rebalance([]);
40
-
41
- expect(explorerSpy.called).to.be.false;
42
- expect(rebalanceSpy.calledOnce).to.be.true;
43
- expect(rebalanceSpy.calledWith([])).to.be.true;
44
- });
45
-
46
- it('calls underlying rebalancer when no inflight is detected', async () => {
47
- const config = buildTestConfig({}, ['ethereum', 'arbitrum']);
48
- const routes: RebalancingRoute[] = [{ origin: 'ethereum' } as any];
49
-
50
- const rebalancer = new MockRebalancer();
51
- const rebalanceSpy = Sinon.spy(rebalancer, 'rebalance');
52
-
53
- const explorer = new ExplorerClient('http://localhost');
54
- const explorerSpy = Sinon.stub(
55
- explorer,
56
- 'hasUndeliveredRebalance',
57
- ).resolves(false);
58
-
59
- const guard = new WithInflightGuard(
60
- config,
61
- rebalancer,
62
- explorer,
63
- ethers.Wallet.createRandom().address,
64
- new ChainMetadataManager(chainMetadata as any),
65
- testLogger,
66
- );
67
-
68
- await guard.rebalance(routes);
69
-
70
- expect(explorerSpy.calledOnce).to.be.true;
71
- expect(rebalanceSpy.calledOnce).to.be.true;
72
- expect(rebalanceSpy.calledWith(routes)).to.be.true;
73
- });
74
-
75
- it('skips rebalancing when inflight is detected', async () => {
76
- const config = buildTestConfig({}, ['ethereum', 'arbitrum']);
77
- const routes: RebalancingRoute[] = [{ origin: 'ethereum' } as any];
78
-
79
- const rebalancer = new MockRebalancer();
80
- const rebalanceSpy = Sinon.spy(rebalancer, 'rebalance');
81
-
82
- const explorer = new ExplorerClient('http://localhost');
83
- const explorerSpy = Sinon.stub(
84
- explorer,
85
- 'hasUndeliveredRebalance',
86
- ).resolves(true);
87
-
88
- const guard = new WithInflightGuard(
89
- config,
90
- rebalancer,
91
- explorer,
92
- ethers.Wallet.createRandom().address,
93
- new ChainMetadataManager(chainMetadata as any),
94
- testLogger,
95
- );
96
-
97
- await guard.rebalance(routes);
98
-
99
- expect(explorerSpy.calledOnce).to.be.true;
100
- expect(rebalanceSpy.called).to.be.false;
101
- });
102
-
103
- it('propagates explorer query error', async () => {
104
- const config = buildTestConfig({}, ['ethereum', 'arbitrum']);
105
- const routes: RebalancingRoute[] = [{ origin: 'ethereum' } as any];
106
-
107
- const rebalancer = new MockRebalancer();
108
- const rebalanceSpy = Sinon.spy(rebalancer, 'rebalance');
109
-
110
- const explorer = new ExplorerClient('http://localhost');
111
- const explorerSpy = Sinon.stub(explorer, 'hasUndeliveredRebalance').rejects(
112
- new Error('Explorer HTTP 405'),
113
- );
114
-
115
- const guard = new WithInflightGuard(
116
- config,
117
- rebalancer,
118
- explorer,
119
- ethers.Wallet.createRandom().address,
120
- new ChainMetadataManager(chainMetadata as any),
121
- testLogger,
122
- );
123
-
124
- await expect(guard.rebalance(routes)).to.be.rejectedWith(
125
- 'Explorer HTTP 405',
126
- );
127
-
128
- expect(explorerSpy.calledOnce).to.be.true;
129
- expect(rebalanceSpy.called).to.be.false;
130
- });
131
- });
@@ -1,67 +0,0 @@
1
- import type { Logger } from 'pino';
2
-
3
- import { type ChainMetadataManager } from '@hyperlane-xyz/sdk';
4
-
5
- import { type RebalancerConfig } from '../config/RebalancerConfig.js';
6
- import type { IRebalancer } from '../interfaces/IRebalancer.js';
7
- import type { RebalancingRoute } from '../interfaces/IStrategy.js';
8
- import { type ExplorerClient } from '../utils/ExplorerClient.js';
9
-
10
- /**
11
- * Prevents rebalancing if there are inflight rebalances for the warp route.
12
- */
13
- export class WithInflightGuard implements IRebalancer {
14
- private readonly logger: Logger;
15
-
16
- constructor(
17
- private readonly config: RebalancerConfig,
18
- private readonly rebalancer: IRebalancer,
19
- private readonly explorer: ExplorerClient,
20
- private readonly txSender: string,
21
- private readonly chainManager: ChainMetadataManager,
22
- logger: Logger,
23
- ) {
24
- this.logger = logger.child({ class: WithInflightGuard.name });
25
- }
26
-
27
- async rebalance(routes: RebalancingRoute[]): Promise<void> {
28
- // Always enforce the inflight guard
29
- if (routes.length === 0) {
30
- return this.rebalancer.rebalance(routes);
31
- }
32
-
33
- const chains = Object.keys(this.config.strategyConfig.chains);
34
- const bridges = chains.map(
35
- (chain) => this.config.strategyConfig.chains[chain].bridge,
36
- );
37
- const domains = chains.map((chain) => this.chainManager.getDomainId(chain));
38
-
39
- let hasInflightRebalances = false;
40
- try {
41
- hasInflightRebalances = await this.explorer.hasUndeliveredRebalance(
42
- {
43
- bridges,
44
- domains: Array.from(new Set(domains)),
45
- txSender: this.txSender,
46
- limit: 5,
47
- },
48
- this.logger,
49
- );
50
- } catch (e: any) {
51
- this.logger.error(
52
- { status: e.status, body: e.body },
53
- 'Explorer inflight query failed',
54
- );
55
- throw e;
56
- }
57
-
58
- if (hasInflightRebalances) {
59
- this.logger.info(
60
- 'Inflight rebalance detected via Explorer; skipping this cycle',
61
- );
62
- return;
63
- }
64
-
65
- return this.rebalancer.rebalance(routes);
66
- }
67
- }
@@ -1,111 +0,0 @@
1
- import chai, { expect } from 'chai';
2
- import chaiAsPromised from 'chai-as-promised';
3
- import { pino } from 'pino';
4
- import Sinon from 'sinon';
5
-
6
- import { RebalancerStrategyOptions } from '../config/types.js';
7
- import { type RebalancingRoute } from '../interfaces/IStrategy.js';
8
- import { MockRebalancer, buildTestConfig } from '../test/helpers.js';
9
-
10
- import { WithSemaphore } from './WithSemaphore.js';
11
-
12
- chai.use(chaiAsPromised);
13
-
14
- const testLogger = pino({ level: 'silent' });
15
-
16
- describe('WithSemaphore', () => {
17
- it('should call the underlying rebalancer', async () => {
18
- const config = buildTestConfig();
19
-
20
- const routes = [
21
- {
22
- origin: 'chain1',
23
- } as any as RebalancingRoute,
24
- ];
25
-
26
- const rebalancer = new MockRebalancer();
27
- const rebalanceSpy = Sinon.spy(rebalancer, 'rebalance');
28
- const withSemaphore = new WithSemaphore(config, rebalancer, testLogger);
29
- await withSemaphore.rebalance(routes);
30
-
31
- expect(rebalanceSpy.calledOnce).to.be.true;
32
- expect(rebalanceSpy.calledWith(routes)).to.be.true;
33
- });
34
-
35
- it('should return early if there are no routes', async () => {
36
- const config = buildTestConfig();
37
-
38
- const rebalancer = new MockRebalancer();
39
- const rebalanceSpy = Sinon.spy(rebalancer, 'rebalance');
40
- const withSemaphore = new WithSemaphore(config, rebalancer, testLogger);
41
- await withSemaphore.rebalance([]);
42
-
43
- expect(rebalanceSpy.calledOnce).to.be.false;
44
- });
45
-
46
- it('should return early if rebalance occurs before waitUntil is reached', async () => {
47
- const config = buildTestConfig();
48
-
49
- const routes = [
50
- {
51
- origin: 'chain1',
52
- } as any as RebalancingRoute,
53
- ];
54
-
55
- const rebalancer = new MockRebalancer();
56
- const rebalanceSpy = Sinon.spy(rebalancer, 'rebalance');
57
- const withSemaphore = new WithSemaphore(config, rebalancer, testLogger);
58
- await withSemaphore.rebalance(routes);
59
-
60
- expect(rebalanceSpy.calledOnce).to.be.true;
61
- expect(rebalanceSpy.calledWith(routes)).to.be.true;
62
-
63
- rebalanceSpy.resetHistory();
64
- await withSemaphore.rebalance(routes);
65
-
66
- expect(rebalanceSpy.calledOnce).to.be.false;
67
- });
68
-
69
- it('should throw if a chain is missing', async () => {
70
- const config = buildTestConfig({
71
- strategyConfig: {
72
- rebalanceStrategy: RebalancerStrategyOptions.Weighted,
73
- chains: {},
74
- },
75
- });
76
-
77
- const routes = [
78
- {
79
- origin: 'chain1',
80
- } as any as RebalancingRoute,
81
- ];
82
-
83
- const rebalancer = new MockRebalancer();
84
- const withSemaphore = new WithSemaphore(config, rebalancer, testLogger);
85
-
86
- await expect(withSemaphore.rebalance(routes)).to.be.rejectedWith(
87
- `Chain ${routes[0].origin} not found in config`,
88
- );
89
- });
90
-
91
- it('should not execute if another rebalance is currently executing', async () => {
92
- const config = buildTestConfig();
93
-
94
- const routes = [
95
- {
96
- origin: 'chain1',
97
- } as any as RebalancingRoute,
98
- ];
99
-
100
- const rebalancer = new MockRebalancer();
101
- const rebalanceSpy = Sinon.spy(rebalancer, 'rebalance');
102
- const withSemaphore = new WithSemaphore(config, rebalancer, testLogger);
103
-
104
- const rebalancePromise1 = withSemaphore.rebalance(routes);
105
- const rebalancePromise2 = withSemaphore.rebalance(routes);
106
- await rebalancePromise1;
107
- await rebalancePromise2;
108
-
109
- expect(rebalanceSpy.calledOnce).to.be.true;
110
- });
111
- });
@@ -1,92 +0,0 @@
1
- import type { Logger } from 'pino';
2
-
3
- import { type RebalancerConfig } from '../config/RebalancerConfig.js';
4
- import type { IRebalancer } from '../interfaces/IRebalancer.js';
5
- import type { RebalancingRoute } from '../interfaces/IStrategy.js';
6
-
7
- /**
8
- * Prevents frequent rebalancing operations while bridges complete.
9
- */
10
- export class WithSemaphore implements IRebalancer {
11
- // Timestamp until which rebalancing should be blocked
12
- private waitUntil: number = 0;
13
- // Lock to prevent concurrent rebalance execution
14
- private executing: boolean = false;
15
- private readonly logger: Logger;
16
-
17
- constructor(
18
- private readonly config: RebalancerConfig,
19
- private readonly rebalancer: IRebalancer,
20
- logger: Logger,
21
- ) {
22
- this.logger = logger.child({ class: WithSemaphore.name });
23
- }
24
-
25
- /**
26
- * Rebalance with timing control
27
- * @param routes - Routes to process
28
- */
29
- async rebalance(routes: RebalancingRoute[]): Promise<void> {
30
- if (this.executing) {
31
- this.logger.info('Currently executing rebalance. Skipping.');
32
-
33
- return;
34
- }
35
-
36
- // No routes mean the system is balanced so we reset the timer to allow new rebalancing
37
- if (!routes.length) {
38
- this.logger.info(
39
- 'No routes to execute. Assuming rebalance is complete. Resetting semaphore timer.',
40
- );
41
-
42
- this.waitUntil = 0;
43
- return;
44
- }
45
-
46
- // Skip if still in waiting period
47
- if (Date.now() < this.waitUntil) {
48
- this.logger.info('Still in waiting period. Skipping rebalance.');
49
-
50
- return;
51
- }
52
-
53
- // The wait period will be determined by the bridge with the highest wait tolerance
54
- const highestTolerance = this.getHighestLockTime(routes);
55
-
56
- try {
57
- // Execute rebalance
58
- this.executing = true;
59
- await this.rebalancer.rebalance(routes);
60
- } finally {
61
- this.executing = false;
62
- }
63
-
64
- // Set new waiting period
65
- this.waitUntil = Date.now() + highestTolerance;
66
-
67
- this.logger.info(
68
- {
69
- highestTolerance,
70
- waitUntil: this.waitUntil,
71
- },
72
- 'Rebalance semaphore locked',
73
- );
74
- }
75
-
76
- private getHighestLockTime(routes: RebalancingRoute[]) {
77
- return routes.reduce((highest, route) => {
78
- const origin = this.config.strategyConfig.chains[route.origin];
79
-
80
- if (!origin) {
81
- this.logger.error({ route }, 'Chain not found in config. Skipping.');
82
- throw new Error(`Chain ${route.origin} not found in config`);
83
- }
84
-
85
- const bridgeLockTime = origin.bridgeLockTime;
86
- const overrideLockTime =
87
- origin.override?.[route.destination]?.bridgeLockTime ?? 0;
88
-
89
- return Math.max(highest, bridgeLockTime, overrideLockTime);
90
- }, 0);
91
- }
92
- }