@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.
- package/README.md +134 -14
- package/dist/config/RebalancerConfig.d.ts +2 -2
- package/dist/config/RebalancerConfig.d.ts.map +1 -1
- package/dist/config/RebalancerConfig.js +4 -3
- package/dist/config/RebalancerConfig.js.map +1 -1
- package/dist/config/RebalancerConfig.test.js +434 -163
- package/dist/config/RebalancerConfig.test.js.map +1 -1
- package/dist/config/types.d.ts +1650 -290
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/types.js +124 -46
- package/dist/config/types.js.map +1 -1
- package/dist/core/Rebalancer.d.ts +14 -7
- package/dist/core/Rebalancer.d.ts.map +1 -1
- package/dist/core/Rebalancer.js +168 -99
- package/dist/core/Rebalancer.js.map +1 -1
- package/dist/core/Rebalancer.test.d.ts +2 -0
- package/dist/core/Rebalancer.test.d.ts.map +1 -0
- package/dist/core/Rebalancer.test.js +391 -0
- package/dist/core/Rebalancer.test.js.map +1 -0
- package/dist/core/RebalancerService.d.ts +16 -2
- package/dist/core/RebalancerService.d.ts.map +1 -1
- package/dist/core/RebalancerService.js +164 -21
- package/dist/core/RebalancerService.js.map +1 -1
- package/dist/core/RebalancerService.test.d.ts +2 -0
- package/dist/core/RebalancerService.test.d.ts.map +1 -0
- package/dist/core/RebalancerService.test.js +809 -0
- package/dist/core/RebalancerService.test.js.map +1 -0
- package/dist/factories/RebalancerContextFactory.d.ts +11 -0
- package/dist/factories/RebalancerContextFactory.d.ts.map +1 -1
- package/dist/factories/RebalancerContextFactory.js +60 -13
- package/dist/factories/RebalancerContextFactory.js.map +1 -1
- package/dist/index.d.ts +6 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/interfaces/IMonitor.d.ts +6 -8
- package/dist/interfaces/IMonitor.d.ts.map +1 -1
- package/dist/interfaces/IMonitor.js.map +1 -1
- package/dist/interfaces/IRebalancer.d.ts +20 -4
- package/dist/interfaces/IRebalancer.d.ts.map +1 -1
- package/dist/interfaces/IStrategy.d.ts +18 -2
- package/dist/interfaces/IStrategy.d.ts.map +1 -1
- package/dist/metrics/Metrics.d.ts +4 -2
- package/dist/metrics/Metrics.d.ts.map +1 -1
- package/dist/metrics/Metrics.js +21 -1
- package/dist/metrics/Metrics.js.map +1 -1
- package/dist/metrics/scripts/metrics.d.ts +2 -0
- package/dist/metrics/scripts/metrics.d.ts.map +1 -1
- package/dist/metrics/scripts/metrics.js +12 -0
- package/dist/metrics/scripts/metrics.js.map +1 -1
- package/dist/monitor/Monitor.d.ts +8 -3
- package/dist/monitor/Monitor.d.ts.map +1 -1
- package/dist/monitor/Monitor.js +75 -15
- package/dist/monitor/Monitor.js.map +1 -1
- package/dist/strategy/BaseStrategy.d.ts +51 -5
- package/dist/strategy/BaseStrategy.d.ts.map +1 -1
- package/dist/strategy/BaseStrategy.js +199 -19
- package/dist/strategy/BaseStrategy.js.map +1 -1
- package/dist/strategy/CollateralDeficitStrategy.d.ts +65 -0
- package/dist/strategy/CollateralDeficitStrategy.d.ts.map +1 -0
- package/dist/strategy/CollateralDeficitStrategy.js +245 -0
- package/dist/strategy/CollateralDeficitStrategy.js.map +1 -0
- package/dist/strategy/CollateralDeficitStrategy.test.d.ts +2 -0
- package/dist/strategy/CollateralDeficitStrategy.test.d.ts.map +1 -0
- package/dist/strategy/CollateralDeficitStrategy.test.js +364 -0
- package/dist/strategy/CollateralDeficitStrategy.test.js.map +1 -0
- package/dist/strategy/CompositeStrategy.d.ts +18 -0
- package/dist/strategy/CompositeStrategy.d.ts.map +1 -0
- package/dist/strategy/CompositeStrategy.js +63 -0
- package/dist/strategy/CompositeStrategy.js.map +1 -0
- package/dist/strategy/CompositeStrategy.test.d.ts +2 -0
- package/dist/strategy/CompositeStrategy.test.d.ts.map +1 -0
- package/dist/strategy/CompositeStrategy.test.js +265 -0
- package/dist/strategy/CompositeStrategy.test.js.map +1 -0
- package/dist/strategy/MinAmountStrategy.d.ts +12 -5
- package/dist/strategy/MinAmountStrategy.d.ts.map +1 -1
- package/dist/strategy/MinAmountStrategy.js +23 -14
- package/dist/strategy/MinAmountStrategy.js.map +1 -1
- package/dist/strategy/MinAmountStrategy.test.js +88 -20
- package/dist/strategy/MinAmountStrategy.test.js.map +1 -1
- package/dist/strategy/StrategyFactory.d.ts +15 -6
- package/dist/strategy/StrategyFactory.d.ts.map +1 -1
- package/dist/strategy/StrategyFactory.js +48 -10
- package/dist/strategy/StrategyFactory.js.map +1 -1
- package/dist/strategy/StrategyFactory.test.js +2 -2
- package/dist/strategy/StrategyFactory.test.js.map +1 -1
- package/dist/strategy/WeightedStrategy.d.ts +13 -4
- package/dist/strategy/WeightedStrategy.d.ts.map +1 -1
- package/dist/strategy/WeightedStrategy.js +18 -6
- package/dist/strategy/WeightedStrategy.js.map +1 -1
- package/dist/strategy/WeightedStrategy.test.js +108 -18
- package/dist/strategy/WeightedStrategy.test.js.map +1 -1
- package/dist/strategy/index.d.ts +2 -0
- package/dist/strategy/index.d.ts.map +1 -1
- package/dist/strategy/index.js +2 -0
- package/dist/strategy/index.js.map +1 -1
- package/dist/test/helpers.d.ts +93 -3
- package/dist/test/helpers.d.ts.map +1 -1
- package/dist/test/helpers.js +267 -10
- package/dist/test/helpers.js.map +1 -1
- package/dist/tracking/ActionTracker.d.ts +49 -0
- package/dist/tracking/ActionTracker.d.ts.map +1 -0
- package/dist/tracking/ActionTracker.js +422 -0
- package/dist/tracking/ActionTracker.js.map +1 -0
- package/dist/tracking/ActionTracker.test.d.ts +2 -0
- package/dist/tracking/ActionTracker.test.d.ts.map +1 -0
- package/dist/tracking/ActionTracker.test.js +637 -0
- package/dist/tracking/ActionTracker.test.js.map +1 -0
- package/dist/tracking/IActionTracker.d.ts +101 -0
- package/dist/tracking/IActionTracker.d.ts.map +1 -0
- package/dist/tracking/IActionTracker.js +2 -0
- package/dist/tracking/IActionTracker.js.map +1 -0
- package/dist/tracking/InflightContextAdapter.d.ts +18 -0
- package/dist/tracking/InflightContextAdapter.d.ts.map +1 -0
- package/dist/tracking/InflightContextAdapter.js +35 -0
- package/dist/tracking/InflightContextAdapter.js.map +1 -0
- package/dist/tracking/InflightContextAdapter.test.d.ts +2 -0
- package/dist/tracking/InflightContextAdapter.test.d.ts.map +1 -0
- package/dist/tracking/InflightContextAdapter.test.js +172 -0
- package/dist/tracking/InflightContextAdapter.test.js.map +1 -0
- package/dist/tracking/index.d.ts +7 -0
- package/dist/tracking/index.d.ts.map +1 -0
- package/dist/tracking/index.js +6 -0
- package/dist/tracking/index.js.map +1 -0
- package/dist/tracking/store/IStore.d.ts +41 -0
- package/dist/tracking/store/IStore.d.ts.map +1 -0
- package/dist/tracking/store/IStore.js +2 -0
- package/dist/tracking/store/IStore.js.map +1 -0
- package/dist/tracking/store/InMemoryStore.d.ts +21 -0
- package/dist/tracking/store/InMemoryStore.d.ts.map +1 -0
- package/dist/tracking/store/InMemoryStore.js +40 -0
- package/dist/tracking/store/InMemoryStore.js.map +1 -0
- package/dist/tracking/store/InMemoryStore.test.d.ts +2 -0
- package/dist/tracking/store/InMemoryStore.test.d.ts.map +1 -0
- package/dist/tracking/store/InMemoryStore.test.js +290 -0
- package/dist/tracking/store/InMemoryStore.test.js.map +1 -0
- package/dist/tracking/store/index.d.ts +3 -0
- package/dist/tracking/store/index.d.ts.map +1 -0
- package/dist/tracking/store/index.js +2 -0
- package/dist/tracking/store/index.js.map +1 -0
- package/dist/tracking/types.d.ts +43 -0
- package/dist/tracking/types.d.ts.map +1 -0
- package/dist/tracking/types.js +2 -0
- package/dist/tracking/types.js.map +1 -0
- package/dist/utils/ExplorerClient.d.ts +39 -1
- package/dist/utils/ExplorerClient.d.ts.map +1 -1
- package/dist/utils/ExplorerClient.js +205 -2
- package/dist/utils/ExplorerClient.js.map +1 -1
- package/dist/utils/balanceUtils.js +2 -2
- package/dist/utils/balanceUtils.js.map +1 -1
- package/dist/utils/balanceUtils.test.js +1 -0
- package/dist/utils/balanceUtils.test.js.map +1 -1
- package/dist/utils/bridgeUtils.d.ts +1 -3
- package/dist/utils/bridgeUtils.d.ts.map +1 -1
- package/dist/utils/bridgeUtils.js +1 -5
- package/dist/utils/bridgeUtils.js.map +1 -1
- package/dist/utils/bridgeUtils.test.js +3 -14
- package/dist/utils/bridgeUtils.test.js.map +1 -1
- package/package.json +11 -9
- package/src/config/RebalancerConfig.test.ts +459 -163
- package/src/config/RebalancerConfig.ts +5 -3
- package/src/config/types.ts +159 -52
- package/src/core/Rebalancer.test.ts +632 -0
- package/src/core/Rebalancer.ts +247 -157
- package/src/core/RebalancerService.test.ts +1144 -0
- package/src/core/RebalancerService.ts +245 -23
- package/src/factories/RebalancerContextFactory.ts +115 -14
- package/src/index.ts +16 -4
- package/src/interfaces/IMonitor.ts +15 -8
- package/src/interfaces/IRebalancer.ts +22 -4
- package/src/interfaces/IStrategy.ts +23 -2
- package/src/metrics/Metrics.ts +26 -5
- package/src/metrics/scripts/metrics.ts +14 -0
- package/src/monitor/Monitor.ts +109 -22
- package/src/strategy/BaseStrategy.ts +316 -26
- package/src/strategy/CollateralDeficitStrategy.test.ts +551 -0
- package/src/strategy/CollateralDeficitStrategy.ts +390 -0
- package/src/strategy/CompositeStrategy.test.ts +405 -0
- package/src/strategy/CompositeStrategy.ts +102 -0
- package/src/strategy/MinAmountStrategy.test.ts +189 -88
- package/src/strategy/MinAmountStrategy.ts +44 -13
- package/src/strategy/StrategyFactory.test.ts +2 -2
- package/src/strategy/StrategyFactory.ts +91 -8
- package/src/strategy/WeightedStrategy.test.ts +187 -72
- package/src/strategy/WeightedStrategy.ts +41 -7
- package/src/strategy/index.ts +2 -0
- package/src/test/helpers.ts +418 -14
- package/src/tracking/ActionTracker.test.ts +783 -0
- package/src/tracking/ActionTracker.ts +647 -0
- package/src/tracking/IActionTracker.ts +140 -0
- package/src/tracking/InflightContextAdapter.test.ts +203 -0
- package/src/tracking/InflightContextAdapter.ts +42 -0
- package/src/tracking/index.ts +36 -0
- package/src/tracking/store/IStore.ts +48 -0
- package/src/tracking/store/InMemoryStore.test.ts +338 -0
- package/src/tracking/store/InMemoryStore.ts +58 -0
- package/src/tracking/store/index.ts +2 -0
- package/src/tracking/types.ts +74 -0
- package/src/utils/ExplorerClient.ts +266 -3
- package/src/utils/balanceUtils.test.ts +1 -0
- package/src/utils/balanceUtils.ts +2 -2
- package/src/utils/bridgeUtils.test.ts +3 -15
- package/src/utils/bridgeUtils.ts +0 -10
- package/dist/core/WithInflightGuard.d.ts +0 -20
- package/dist/core/WithInflightGuard.d.ts.map +0 -1
- package/dist/core/WithInflightGuard.js +0 -47
- package/dist/core/WithInflightGuard.js.map +0 -1
- package/dist/core/WithInflightGuard.test.d.ts +0 -2
- package/dist/core/WithInflightGuard.test.d.ts.map +0 -1
- package/dist/core/WithInflightGuard.test.js +0 -64
- package/dist/core/WithInflightGuard.test.js.map +0 -1
- package/dist/core/WithSemaphore.d.ts +0 -22
- package/dist/core/WithSemaphore.d.ts.map +0 -1
- package/dist/core/WithSemaphore.js +0 -67
- package/dist/core/WithSemaphore.js.map +0 -1
- package/dist/core/WithSemaphore.test.d.ts +0 -2
- package/dist/core/WithSemaphore.test.d.ts.map +0 -1
- package/dist/core/WithSemaphore.test.js +0 -83
- package/dist/core/WithSemaphore.test.js.map +0 -1
- package/src/core/WithInflightGuard.test.ts +0 -131
- package/src/core/WithInflightGuard.ts +0 -67
- package/src/core/WithSemaphore.test.ts +0 -111
- package/src/core/WithSemaphore.ts +0 -92
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
import { expect } from 'chai';
|
|
2
|
+
import { pino } from 'pino';
|
|
3
|
+
|
|
4
|
+
import type { ChainName } from '@hyperlane-xyz/sdk';
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
IStrategy,
|
|
8
|
+
InflightContext,
|
|
9
|
+
RawBalances,
|
|
10
|
+
Route,
|
|
11
|
+
StrategyRoute,
|
|
12
|
+
} from '../interfaces/IStrategy.js';
|
|
13
|
+
|
|
14
|
+
import { CompositeStrategy } from './CompositeStrategy.js';
|
|
15
|
+
|
|
16
|
+
const testLogger = pino({ level: 'silent' });
|
|
17
|
+
const TEST_BRIDGE = '0x1234567890123456789012345678901234567890';
|
|
18
|
+
|
|
19
|
+
class MockStrategy implements IStrategy {
|
|
20
|
+
readonly name = 'mock';
|
|
21
|
+
public lastInflightContext?: InflightContext;
|
|
22
|
+
|
|
23
|
+
constructor(private readonly routesToReturn: StrategyRoute[]) {}
|
|
24
|
+
|
|
25
|
+
getRebalancingRoutes(
|
|
26
|
+
_rawBalances: RawBalances,
|
|
27
|
+
inflightContext?: InflightContext,
|
|
28
|
+
): StrategyRoute[] {
|
|
29
|
+
this.lastInflightContext = inflightContext;
|
|
30
|
+
return this.routesToReturn;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe('CompositeStrategy', () => {
|
|
35
|
+
let chain1: ChainName;
|
|
36
|
+
let chain2: ChainName;
|
|
37
|
+
let chain3: ChainName;
|
|
38
|
+
|
|
39
|
+
beforeEach(() => {
|
|
40
|
+
chain1 = 'chain1';
|
|
41
|
+
chain2 = 'chain2';
|
|
42
|
+
chain3 = 'chain3';
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('constructor', () => {
|
|
46
|
+
it('should throw an error when less than 2 strategies are provided', () => {
|
|
47
|
+
const mockStrategy = new MockStrategy([]);
|
|
48
|
+
|
|
49
|
+
expect(() => new CompositeStrategy([mockStrategy], testLogger)).to.throw(
|
|
50
|
+
'CompositeStrategy requires at least 2 sub-strategies',
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should throw an error when no strategies are provided', () => {
|
|
55
|
+
expect(() => new CompositeStrategy([], testLogger)).to.throw(
|
|
56
|
+
'CompositeStrategy requires at least 2 sub-strategies',
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should create a strategy with 2+ sub-strategies', () => {
|
|
61
|
+
const strategy1 = new MockStrategy([]);
|
|
62
|
+
const strategy2 = new MockStrategy([]);
|
|
63
|
+
|
|
64
|
+
const composite = new CompositeStrategy(
|
|
65
|
+
[strategy1, strategy2],
|
|
66
|
+
testLogger,
|
|
67
|
+
);
|
|
68
|
+
expect(composite).to.be.instanceOf(CompositeStrategy);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('getRebalancingRoutes', () => {
|
|
73
|
+
it('should concatenate routes from all sub-strategies', () => {
|
|
74
|
+
const route1: StrategyRoute = {
|
|
75
|
+
origin: chain1,
|
|
76
|
+
destination: chain2,
|
|
77
|
+
amount: 1000n,
|
|
78
|
+
bridge: TEST_BRIDGE,
|
|
79
|
+
};
|
|
80
|
+
const route2: StrategyRoute = {
|
|
81
|
+
origin: chain2,
|
|
82
|
+
destination: chain3,
|
|
83
|
+
amount: 2000n,
|
|
84
|
+
bridge: TEST_BRIDGE,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const strategy1 = new MockStrategy([route1]);
|
|
88
|
+
const strategy2 = new MockStrategy([route2]);
|
|
89
|
+
|
|
90
|
+
const composite = new CompositeStrategy(
|
|
91
|
+
[strategy1, strategy2],
|
|
92
|
+
testLogger,
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const rawBalances: RawBalances = {
|
|
96
|
+
[chain1]: 5000n,
|
|
97
|
+
[chain2]: 10000n,
|
|
98
|
+
[chain3]: 3000n,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const routes = composite.getRebalancingRoutes(rawBalances);
|
|
102
|
+
|
|
103
|
+
expect(routes).to.have.lengthOf(2);
|
|
104
|
+
expect(routes[0]).to.deep.equal(route1);
|
|
105
|
+
expect(routes[1]).to.deep.equal(route2);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should pass routes from earlier strategies as proposedRebalances to later strategies', () => {
|
|
109
|
+
const route1: StrategyRoute = {
|
|
110
|
+
origin: chain1,
|
|
111
|
+
destination: chain2,
|
|
112
|
+
amount: 1000n,
|
|
113
|
+
bridge: TEST_BRIDGE,
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const strategy1 = new MockStrategy([route1]);
|
|
117
|
+
const strategy2 = new MockStrategy([]);
|
|
118
|
+
|
|
119
|
+
const composite = new CompositeStrategy(
|
|
120
|
+
[strategy1, strategy2],
|
|
121
|
+
testLogger,
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
const rawBalances: RawBalances = {
|
|
125
|
+
[chain1]: 5000n,
|
|
126
|
+
[chain2]: 10000n,
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
composite.getRebalancingRoutes(rawBalances);
|
|
130
|
+
|
|
131
|
+
// Strategy 1 should receive empty proposedRebalances (none from earlier strategies)
|
|
132
|
+
expect(strategy1.lastInflightContext?.proposedRebalances).to.deep.equal(
|
|
133
|
+
[],
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
// Strategy 2 should receive route1 as proposedRebalances (from earlier strategy)
|
|
137
|
+
expect(
|
|
138
|
+
strategy2.lastInflightContext?.proposedRebalances,
|
|
139
|
+
).to.have.lengthOf(1);
|
|
140
|
+
expect(
|
|
141
|
+
strategy2.lastInflightContext?.proposedRebalances?.[0],
|
|
142
|
+
).to.deep.equal(route1);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should accumulate routes across multiple strategies as proposedRebalances', () => {
|
|
146
|
+
const route1: StrategyRoute = {
|
|
147
|
+
origin: chain1,
|
|
148
|
+
destination: chain2,
|
|
149
|
+
amount: 1000n,
|
|
150
|
+
bridge: TEST_BRIDGE,
|
|
151
|
+
};
|
|
152
|
+
const route2: StrategyRoute = {
|
|
153
|
+
origin: chain2,
|
|
154
|
+
destination: chain3,
|
|
155
|
+
amount: 2000n,
|
|
156
|
+
bridge: TEST_BRIDGE,
|
|
157
|
+
};
|
|
158
|
+
const route3: StrategyRoute = {
|
|
159
|
+
origin: chain3,
|
|
160
|
+
destination: chain1,
|
|
161
|
+
amount: 3000n,
|
|
162
|
+
bridge: TEST_BRIDGE,
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const strategy1 = new MockStrategy([route1]);
|
|
166
|
+
const strategy2 = new MockStrategy([route2]);
|
|
167
|
+
const strategy3 = new MockStrategy([route3]);
|
|
168
|
+
|
|
169
|
+
const composite = new CompositeStrategy(
|
|
170
|
+
[strategy1, strategy2, strategy3],
|
|
171
|
+
testLogger,
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
const rawBalances: RawBalances = {
|
|
175
|
+
[chain1]: 5000n,
|
|
176
|
+
[chain2]: 10000n,
|
|
177
|
+
[chain3]: 3000n,
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
composite.getRebalancingRoutes(rawBalances);
|
|
181
|
+
|
|
182
|
+
// Strategy 1: empty proposedRebalances (no earlier strategies)
|
|
183
|
+
expect(strategy1.lastInflightContext?.proposedRebalances).to.deep.equal(
|
|
184
|
+
[],
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
// Strategy 2: receives route1 as proposedRebalances
|
|
188
|
+
expect(
|
|
189
|
+
strategy2.lastInflightContext?.proposedRebalances,
|
|
190
|
+
).to.have.lengthOf(1);
|
|
191
|
+
|
|
192
|
+
// Strategy 3: receives route1 + route2 as proposedRebalances
|
|
193
|
+
expect(
|
|
194
|
+
strategy3.lastInflightContext?.proposedRebalances,
|
|
195
|
+
).to.have.lengthOf(2);
|
|
196
|
+
expect(
|
|
197
|
+
strategy3.lastInflightContext?.proposedRebalances?.[0],
|
|
198
|
+
).to.deep.equal(route1);
|
|
199
|
+
expect(
|
|
200
|
+
strategy3.lastInflightContext?.proposedRebalances?.[1],
|
|
201
|
+
).to.deep.equal(route2);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('should preserve original pendingRebalances and use proposedRebalances for new routes', () => {
|
|
205
|
+
const originalPendingRebalance: StrategyRoute = {
|
|
206
|
+
origin: chain3,
|
|
207
|
+
destination: chain1,
|
|
208
|
+
amount: 500n,
|
|
209
|
+
bridge: TEST_BRIDGE,
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const route1: StrategyRoute = {
|
|
213
|
+
origin: chain1,
|
|
214
|
+
destination: chain2,
|
|
215
|
+
amount: 1000n,
|
|
216
|
+
bridge: TEST_BRIDGE,
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const strategy1 = new MockStrategy([route1]);
|
|
220
|
+
const strategy2 = new MockStrategy([]);
|
|
221
|
+
|
|
222
|
+
const composite = new CompositeStrategy(
|
|
223
|
+
[strategy1, strategy2],
|
|
224
|
+
testLogger,
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
const rawBalances: RawBalances = {
|
|
228
|
+
[chain1]: 5000n,
|
|
229
|
+
[chain2]: 10000n,
|
|
230
|
+
[chain3]: 3000n,
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const inflightContext: InflightContext = {
|
|
234
|
+
pendingTransfers: [],
|
|
235
|
+
pendingRebalances: [originalPendingRebalance],
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
composite.getRebalancingRoutes(rawBalances, inflightContext);
|
|
239
|
+
|
|
240
|
+
// Both strategies should receive the SAME original pendingRebalances (inflight intents)
|
|
241
|
+
expect(strategy1.lastInflightContext?.pendingRebalances).to.have.lengthOf(
|
|
242
|
+
1,
|
|
243
|
+
);
|
|
244
|
+
expect(strategy1.lastInflightContext?.pendingRebalances[0]).to.deep.equal(
|
|
245
|
+
originalPendingRebalance,
|
|
246
|
+
);
|
|
247
|
+
expect(strategy2.lastInflightContext?.pendingRebalances).to.have.lengthOf(
|
|
248
|
+
1,
|
|
249
|
+
);
|
|
250
|
+
expect(strategy2.lastInflightContext?.pendingRebalances[0]).to.deep.equal(
|
|
251
|
+
originalPendingRebalance,
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
// Strategy 1: empty proposedRebalances (no earlier strategies)
|
|
255
|
+
expect(strategy1.lastInflightContext?.proposedRebalances).to.deep.equal(
|
|
256
|
+
[],
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
// Strategy 2: receives route1 as proposedRebalances (from earlier strategy)
|
|
260
|
+
expect(
|
|
261
|
+
strategy2.lastInflightContext?.proposedRebalances,
|
|
262
|
+
).to.have.lengthOf(1);
|
|
263
|
+
expect(
|
|
264
|
+
strategy2.lastInflightContext?.proposedRebalances?.[0],
|
|
265
|
+
).to.deep.equal(route1);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('should preserve pendingTransfers for all strategies', () => {
|
|
269
|
+
const pendingTransfer: Route = {
|
|
270
|
+
origin: chain1,
|
|
271
|
+
destination: chain2,
|
|
272
|
+
amount: 500n,
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
const strategy1 = new MockStrategy([]);
|
|
276
|
+
const strategy2 = new MockStrategy([]);
|
|
277
|
+
|
|
278
|
+
const composite = new CompositeStrategy(
|
|
279
|
+
[strategy1, strategy2],
|
|
280
|
+
testLogger,
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
const rawBalances: RawBalances = {
|
|
284
|
+
[chain1]: 5000n,
|
|
285
|
+
[chain2]: 10000n,
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
const inflightContext: InflightContext = {
|
|
289
|
+
pendingTransfers: [pendingTransfer],
|
|
290
|
+
pendingRebalances: [],
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
composite.getRebalancingRoutes(rawBalances, inflightContext);
|
|
294
|
+
|
|
295
|
+
// Both strategies should receive the same pendingTransfers
|
|
296
|
+
expect(strategy1.lastInflightContext?.pendingTransfers).to.deep.equal([
|
|
297
|
+
pendingTransfer,
|
|
298
|
+
]);
|
|
299
|
+
expect(strategy2.lastInflightContext?.pendingTransfers).to.deep.equal([
|
|
300
|
+
pendingTransfer,
|
|
301
|
+
]);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it('should maintain route order (first strategy routes come first)', () => {
|
|
305
|
+
const route1a: StrategyRoute = {
|
|
306
|
+
origin: chain1,
|
|
307
|
+
destination: chain2,
|
|
308
|
+
amount: 1000n,
|
|
309
|
+
bridge: TEST_BRIDGE,
|
|
310
|
+
};
|
|
311
|
+
const route1b: StrategyRoute = {
|
|
312
|
+
origin: chain1,
|
|
313
|
+
destination: chain3,
|
|
314
|
+
amount: 1500n,
|
|
315
|
+
bridge: TEST_BRIDGE,
|
|
316
|
+
};
|
|
317
|
+
const route2a: StrategyRoute = {
|
|
318
|
+
origin: chain2,
|
|
319
|
+
destination: chain3,
|
|
320
|
+
amount: 2000n,
|
|
321
|
+
bridge: TEST_BRIDGE,
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
const strategy1 = new MockStrategy([route1a, route1b]);
|
|
325
|
+
const strategy2 = new MockStrategy([route2a]);
|
|
326
|
+
|
|
327
|
+
const composite = new CompositeStrategy(
|
|
328
|
+
[strategy1, strategy2],
|
|
329
|
+
testLogger,
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
const rawBalances: RawBalances = {
|
|
333
|
+
[chain1]: 5000n,
|
|
334
|
+
[chain2]: 10000n,
|
|
335
|
+
[chain3]: 3000n,
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
const routes = composite.getRebalancingRoutes(rawBalances);
|
|
339
|
+
|
|
340
|
+
expect(routes).to.have.lengthOf(3);
|
|
341
|
+
expect(routes[0]).to.deep.equal(route1a);
|
|
342
|
+
expect(routes[1]).to.deep.equal(route1b);
|
|
343
|
+
expect(routes[2]).to.deep.equal(route2a);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it('should handle strategies that return no routes', () => {
|
|
347
|
+
const route2: StrategyRoute = {
|
|
348
|
+
origin: chain2,
|
|
349
|
+
destination: chain3,
|
|
350
|
+
amount: 2000n,
|
|
351
|
+
bridge: TEST_BRIDGE,
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
const strategy1 = new MockStrategy([]);
|
|
355
|
+
const strategy2 = new MockStrategy([route2]);
|
|
356
|
+
const strategy3 = new MockStrategy([]);
|
|
357
|
+
|
|
358
|
+
const composite = new CompositeStrategy(
|
|
359
|
+
[strategy1, strategy2, strategy3],
|
|
360
|
+
testLogger,
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
const rawBalances: RawBalances = {
|
|
364
|
+
[chain1]: 5000n,
|
|
365
|
+
[chain2]: 10000n,
|
|
366
|
+
[chain3]: 3000n,
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
const routes = composite.getRebalancingRoutes(rawBalances);
|
|
370
|
+
|
|
371
|
+
expect(routes).to.have.lengthOf(1);
|
|
372
|
+
expect(routes[0]).to.deep.equal(route2);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it('should handle undefined inflightContext', () => {
|
|
376
|
+
const route1: StrategyRoute = {
|
|
377
|
+
origin: chain1,
|
|
378
|
+
destination: chain2,
|
|
379
|
+
amount: 1000n,
|
|
380
|
+
bridge: TEST_BRIDGE,
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
const strategy1 = new MockStrategy([route1]);
|
|
384
|
+
const strategy2 = new MockStrategy([]);
|
|
385
|
+
|
|
386
|
+
const composite = new CompositeStrategy(
|
|
387
|
+
[strategy1, strategy2],
|
|
388
|
+
testLogger,
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
const rawBalances: RawBalances = {
|
|
392
|
+
[chain1]: 5000n,
|
|
393
|
+
[chain2]: 10000n,
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
const routes = composite.getRebalancingRoutes(rawBalances, undefined);
|
|
397
|
+
|
|
398
|
+
expect(routes).to.have.lengthOf(1);
|
|
399
|
+
expect(strategy1.lastInflightContext?.pendingTransfers).to.deep.equal([]);
|
|
400
|
+
expect(strategy1.lastInflightContext?.pendingRebalances).to.deep.equal(
|
|
401
|
+
[],
|
|
402
|
+
);
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
});
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { Logger } from 'pino';
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
IStrategy,
|
|
5
|
+
InflightContext,
|
|
6
|
+
RawBalances,
|
|
7
|
+
StrategyRoute,
|
|
8
|
+
} from '../interfaces/IStrategy.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Composite strategy that runs multiple sub-strategies sequentially.
|
|
12
|
+
*
|
|
13
|
+
* Key behavior: Routes from earlier strategies are passed as proposedRebalances
|
|
14
|
+
* to later strategies, allowing coordination between strategies.
|
|
15
|
+
*
|
|
16
|
+
* Requires at least 2 sub-strategies.
|
|
17
|
+
*/
|
|
18
|
+
export class CompositeStrategy implements IStrategy {
|
|
19
|
+
readonly name = 'composite';
|
|
20
|
+
protected readonly logger: Logger;
|
|
21
|
+
|
|
22
|
+
constructor(
|
|
23
|
+
private readonly strategies: IStrategy[],
|
|
24
|
+
logger: Logger,
|
|
25
|
+
) {
|
|
26
|
+
if (strategies.length < 2) {
|
|
27
|
+
throw new Error('CompositeStrategy requires at least 2 sub-strategies');
|
|
28
|
+
}
|
|
29
|
+
this.logger = logger.child({ class: CompositeStrategy.name });
|
|
30
|
+
this.logger.info(
|
|
31
|
+
{
|
|
32
|
+
strategyCount: strategies.length,
|
|
33
|
+
strategies: strategies.map((s) => s.name),
|
|
34
|
+
},
|
|
35
|
+
'CompositeStrategy created',
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
getRebalancingRoutes(
|
|
40
|
+
rawBalances: RawBalances,
|
|
41
|
+
inflightContext?: InflightContext,
|
|
42
|
+
): StrategyRoute[] {
|
|
43
|
+
const allRoutes: StrategyRoute[] = [];
|
|
44
|
+
|
|
45
|
+
// Track routes from earlier strategies in this cycle as proposedRebalances
|
|
46
|
+
// These are NOT yet executed, so strategies need to simulate both origin and destination
|
|
47
|
+
let accumulatedProposedRebalances: StrategyRoute[] = [];
|
|
48
|
+
|
|
49
|
+
for (let i = 0; i < this.strategies.length; i++) {
|
|
50
|
+
const strategy = this.strategies[i];
|
|
51
|
+
|
|
52
|
+
// Build context with:
|
|
53
|
+
// - pendingRebalances: actual in-flight intents (origin tx confirmed, passed through from caller)
|
|
54
|
+
// - proposedRebalances: routes from earlier strategies in THIS cycle (not yet executed)
|
|
55
|
+
const contextForStrategy: InflightContext = {
|
|
56
|
+
pendingTransfers: inflightContext?.pendingTransfers ?? [],
|
|
57
|
+
pendingRebalances: inflightContext?.pendingRebalances ?? [],
|
|
58
|
+
proposedRebalances: accumulatedProposedRebalances,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
this.logger.debug(
|
|
62
|
+
{
|
|
63
|
+
strategyIndex: i,
|
|
64
|
+
strategyName: strategy.name,
|
|
65
|
+
pendingRebalancesCount: contextForStrategy.pendingRebalances.length,
|
|
66
|
+
proposedRebalancesCount: accumulatedProposedRebalances.length,
|
|
67
|
+
},
|
|
68
|
+
'Running sub-strategy',
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const routes = strategy.getRebalancingRoutes(
|
|
72
|
+
rawBalances,
|
|
73
|
+
contextForStrategy,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
this.logger.debug(
|
|
77
|
+
{
|
|
78
|
+
strategyIndex: i,
|
|
79
|
+
strategyName: strategy.name,
|
|
80
|
+
routeCount: routes.length,
|
|
81
|
+
},
|
|
82
|
+
'Sub-strategy returned routes',
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
// Add routes to proposedRebalances for next strategy
|
|
86
|
+
accumulatedProposedRebalances = [
|
|
87
|
+
...accumulatedProposedRebalances,
|
|
88
|
+
...routes,
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
// Add to final result
|
|
92
|
+
allRoutes.push(...routes);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
this.logger.info(
|
|
96
|
+
{ totalRoutes: allRoutes.length },
|
|
97
|
+
'CompositeStrategy merged routes from all sub-strategies',
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
return allRoutes;
|
|
101
|
+
}
|
|
102
|
+
}
|