@hyperlane-xyz/rebalancer 0.1.0-beta.5a8bd28ab
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 +178 -0
- package/dist/config/RebalancerConfig.d.ts +12 -0
- package/dist/config/RebalancerConfig.d.ts.map +1 -0
- package/dist/config/RebalancerConfig.js +29 -0
- package/dist/config/RebalancerConfig.js.map +1 -0
- package/dist/config/RebalancerConfig.test.d.ts +2 -0
- package/dist/config/RebalancerConfig.test.d.ts.map +1 -0
- package/dist/config/RebalancerConfig.test.js +325 -0
- package/dist/config/RebalancerConfig.test.js.map +1 -0
- package/dist/core/Rebalancer.d.ts +23 -0
- package/dist/core/Rebalancer.d.ts.map +1 -0
- package/dist/core/Rebalancer.js +290 -0
- package/dist/core/Rebalancer.js.map +1 -0
- package/dist/core/RebalancerService.d.ts +115 -0
- package/dist/core/RebalancerService.d.ts.map +1 -0
- package/dist/core/RebalancerService.js +227 -0
- package/dist/core/RebalancerService.js.map +1 -0
- package/dist/core/WithInflightGuard.d.ts +20 -0
- package/dist/core/WithInflightGuard.d.ts.map +1 -0
- package/dist/core/WithInflightGuard.js +47 -0
- package/dist/core/WithInflightGuard.js.map +1 -0
- package/dist/core/WithInflightGuard.test.d.ts +2 -0
- package/dist/core/WithInflightGuard.test.d.ts.map +1 -0
- package/dist/core/WithInflightGuard.test.js +64 -0
- package/dist/core/WithInflightGuard.test.js.map +1 -0
- package/dist/core/WithSemaphore.d.ts +22 -0
- package/dist/core/WithSemaphore.d.ts.map +1 -0
- package/dist/core/WithSemaphore.js +67 -0
- package/dist/core/WithSemaphore.js.map +1 -0
- package/dist/core/WithSemaphore.test.d.ts +2 -0
- package/dist/core/WithSemaphore.test.d.ts.map +1 -0
- package/dist/core/WithSemaphore.test.js +83 -0
- package/dist/core/WithSemaphore.test.js.map +1 -0
- package/dist/factories/RebalancerContextFactory.d.ts +41 -0
- package/dist/factories/RebalancerContextFactory.d.ts.map +1 -0
- package/dist/factories/RebalancerContextFactory.js +115 -0
- package/dist/factories/RebalancerContextFactory.js.map +1 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +35 -0
- package/dist/index.js.map +1 -0
- package/dist/interfaces/IMetrics.d.ts +5 -0
- package/dist/interfaces/IMetrics.d.ts.map +1 -0
- package/dist/interfaces/IMetrics.js +2 -0
- package/dist/interfaces/IMetrics.js.map +1 -0
- package/dist/interfaces/IMonitor.d.ts +51 -0
- package/dist/interfaces/IMonitor.d.ts.map +1 -0
- package/dist/interfaces/IMonitor.js +14 -0
- package/dist/interfaces/IMonitor.js.map +1 -0
- package/dist/interfaces/IRebalancer.d.ts +15 -0
- package/dist/interfaces/IRebalancer.d.ts.map +1 -0
- package/dist/interfaces/IRebalancer.js +2 -0
- package/dist/interfaces/IRebalancer.js.map +1 -0
- package/dist/interfaces/IStrategy.d.ts +11 -0
- package/dist/interfaces/IStrategy.d.ts.map +1 -0
- package/dist/interfaces/IStrategy.js +2 -0
- package/dist/interfaces/IStrategy.js.map +1 -0
- package/dist/metrics/Metrics.d.ts +31 -0
- package/dist/metrics/Metrics.d.ts.map +1 -0
- package/dist/metrics/Metrics.js +302 -0
- package/dist/metrics/Metrics.js.map +1 -0
- package/dist/metrics/PriceGetter.d.ts +10 -0
- package/dist/metrics/PriceGetter.d.ts.map +1 -0
- package/dist/metrics/PriceGetter.js +41 -0
- package/dist/metrics/PriceGetter.js.map +1 -0
- package/dist/metrics/scripts/metrics.d.ts +14 -0
- package/dist/metrics/scripts/metrics.d.ts.map +1 -0
- package/dist/metrics/scripts/metrics.js +198 -0
- package/dist/metrics/scripts/metrics.js.map +1 -0
- package/dist/metrics/types.d.ts +24 -0
- package/dist/metrics/types.d.ts.map +1 -0
- package/dist/metrics/types.js +2 -0
- package/dist/metrics/types.js.map +1 -0
- package/dist/metrics/utils/metrics.d.ts +12 -0
- package/dist/metrics/utils/metrics.d.ts.map +1 -0
- package/dist/metrics/utils/metrics.js +28 -0
- package/dist/metrics/utils/metrics.js.map +1 -0
- package/dist/monitor/Monitor.d.ts +26 -0
- package/dist/monitor/Monitor.d.ts.map +1 -0
- package/dist/monitor/Monitor.js +116 -0
- package/dist/monitor/Monitor.js.map +1 -0
- package/dist/service.d.ts +3 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +125 -0
- package/dist/service.js.map +1 -0
- package/dist/strategy/BaseStrategy.d.ts +34 -0
- package/dist/strategy/BaseStrategy.d.ts.map +1 -0
- package/dist/strategy/BaseStrategy.js +127 -0
- package/dist/strategy/BaseStrategy.js.map +1 -0
- package/dist/strategy/MinAmountStrategy.d.ts +27 -0
- package/dist/strategy/MinAmountStrategy.d.ts.map +1 -0
- package/dist/strategy/MinAmountStrategy.js +103 -0
- package/dist/strategy/MinAmountStrategy.js.map +1 -0
- package/dist/strategy/MinAmountStrategy.test.d.ts +2 -0
- package/dist/strategy/MinAmountStrategy.test.d.ts.map +1 -0
- package/dist/strategy/MinAmountStrategy.test.js +472 -0
- package/dist/strategy/MinAmountStrategy.test.js.map +1 -0
- package/dist/strategy/StrategyFactory.d.ts +16 -0
- package/dist/strategy/StrategyFactory.d.ts.map +1 -0
- package/dist/strategy/StrategyFactory.js +25 -0
- package/dist/strategy/StrategyFactory.js.map +1 -0
- package/dist/strategy/StrategyFactory.test.d.ts +2 -0
- package/dist/strategy/StrategyFactory.test.d.ts.map +1 -0
- package/dist/strategy/StrategyFactory.test.js +80 -0
- package/dist/strategy/StrategyFactory.test.js.map +1 -0
- package/dist/strategy/WeightedStrategy.d.ts +23 -0
- package/dist/strategy/WeightedStrategy.d.ts.map +1 -0
- package/dist/strategy/WeightedStrategy.js +61 -0
- package/dist/strategy/WeightedStrategy.js.map +1 -0
- package/dist/strategy/WeightedStrategy.test.d.ts +2 -0
- package/dist/strategy/WeightedStrategy.test.d.ts.map +1 -0
- package/dist/strategy/WeightedStrategy.test.js +307 -0
- package/dist/strategy/WeightedStrategy.test.js.map +1 -0
- package/dist/strategy/index.d.ts +5 -0
- package/dist/strategy/index.d.ts.map +1 -0
- package/dist/strategy/index.js +5 -0
- package/dist/strategy/index.js.map +1 -0
- package/dist/test/helpers.d.ts +8 -0
- package/dist/test/helpers.d.ts.map +1 -0
- package/dist/test/helpers.js +33 -0
- package/dist/test/helpers.js.map +1 -0
- package/dist/utils/ExplorerClient.d.ts +14 -0
- package/dist/utils/ExplorerClient.d.ts.map +1 -0
- package/dist/utils/ExplorerClient.js +82 -0
- package/dist/utils/ExplorerClient.js.map +1 -0
- package/dist/utils/balanceUtils.d.ts +13 -0
- package/dist/utils/balanceUtils.d.ts.map +1 -0
- package/dist/utils/balanceUtils.js +43 -0
- package/dist/utils/balanceUtils.js.map +1 -0
- package/dist/utils/balanceUtils.test.d.ts +2 -0
- package/dist/utils/balanceUtils.test.d.ts.map +1 -0
- package/dist/utils/balanceUtils.test.js +54 -0
- package/dist/utils/balanceUtils.test.js.map +1 -0
- package/dist/utils/bridgeUtils.d.ts +19 -0
- package/dist/utils/bridgeUtils.d.ts.map +1 -0
- package/dist/utils/bridgeUtils.js +20 -0
- package/dist/utils/bridgeUtils.js.map +1 -0
- package/dist/utils/bridgeUtils.test.d.ts +2 -0
- package/dist/utils/bridgeUtils.test.d.ts.map +1 -0
- package/dist/utils/bridgeUtils.test.js +77 -0
- package/dist/utils/bridgeUtils.test.js.map +1 -0
- package/dist/utils/errors.d.ts +4 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +6 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/files.d.ts +35 -0
- package/dist/utils/files.d.ts.map +1 -0
- package/dist/utils/files.js +190 -0
- package/dist/utils/files.js.map +1 -0
- package/dist/utils/generalUtils.d.ts +3 -0
- package/dist/utils/generalUtils.d.ts.map +1 -0
- package/dist/utils/generalUtils.js +9 -0
- package/dist/utils/generalUtils.js.map +1 -0
- package/dist/utils/index.d.ts +5 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +5 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/tokenUtils.d.ts +14 -0
- package/dist/utils/tokenUtils.d.ts.map +1 -0
- package/dist/utils/tokenUtils.js +21 -0
- package/dist/utils/tokenUtils.js.map +1 -0
- package/package.json +70 -0
- package/src/config/RebalancerConfig.test.ts +388 -0
- package/src/config/RebalancerConfig.ts +39 -0
- package/src/core/Rebalancer.ts +471 -0
- package/src/core/RebalancerService.ts +333 -0
- package/src/core/WithInflightGuard.test.ts +131 -0
- package/src/core/WithInflightGuard.ts +67 -0
- package/src/core/WithSemaphore.test.ts +112 -0
- package/src/core/WithSemaphore.ts +92 -0
- package/src/factories/RebalancerContextFactory.ts +210 -0
- package/src/index.ts +68 -0
- package/src/interfaces/IMetrics.ts +5 -0
- package/src/interfaces/IMonitor.ts +63 -0
- package/src/interfaces/IRebalancer.ts +20 -0
- package/src/interfaces/IStrategy.ts +13 -0
- package/src/metrics/Metrics.ts +558 -0
- package/src/metrics/PriceGetter.ts +74 -0
- package/src/metrics/scripts/metrics.ts +298 -0
- package/src/metrics/types.ts +27 -0
- package/src/metrics/utils/metrics.ts +33 -0
- package/src/monitor/Monitor.ts +174 -0
- package/src/service.ts +154 -0
- package/src/strategy/BaseStrategy.ts +210 -0
- package/src/strategy/MinAmountStrategy.test.ts +625 -0
- package/src/strategy/MinAmountStrategy.ts +170 -0
- package/src/strategy/StrategyFactory.test.ts +109 -0
- package/src/strategy/StrategyFactory.ts +48 -0
- package/src/strategy/WeightedStrategy.test.ts +408 -0
- package/src/strategy/WeightedStrategy.ts +93 -0
- package/src/strategy/index.ts +4 -0
- package/src/test/helpers.ts +46 -0
- package/src/utils/ExplorerClient.ts +99 -0
- package/src/utils/balanceUtils.test.ts +74 -0
- package/src/utils/balanceUtils.ts +69 -0
- package/src/utils/bridgeUtils.test.ts +92 -0
- package/src/utils/bridgeUtils.ts +42 -0
- package/src/utils/errors.ts +5 -0
- package/src/utils/files.ts +276 -0
- package/src/utils/generalUtils.ts +13 -0
- package/src/utils/index.ts +4 -0
- package/src/utils/tokenUtils.ts +26 -0
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
import { expect } from 'chai';
|
|
2
|
+
import { ethers } from 'ethers';
|
|
3
|
+
import { pino } from 'pino';
|
|
4
|
+
|
|
5
|
+
import type { ChainName } from '@hyperlane-xyz/sdk';
|
|
6
|
+
|
|
7
|
+
import type { RawBalances } from '../interfaces/IStrategy.js';
|
|
8
|
+
|
|
9
|
+
import { WeightedStrategy } from './WeightedStrategy.js';
|
|
10
|
+
|
|
11
|
+
const testLogger = pino({ level: 'silent' });
|
|
12
|
+
|
|
13
|
+
describe('WeightedStrategy', () => {
|
|
14
|
+
let chain1: ChainName;
|
|
15
|
+
let chain2: ChainName;
|
|
16
|
+
let chain3: ChainName;
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
chain1 = 'chain1';
|
|
20
|
+
chain2 = 'chain2';
|
|
21
|
+
chain3 = 'chain3';
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('constructor', () => {
|
|
25
|
+
it('should throw an error when less than two chains are configured', () => {
|
|
26
|
+
expect(
|
|
27
|
+
() =>
|
|
28
|
+
new WeightedStrategy(
|
|
29
|
+
{
|
|
30
|
+
[chain1]: {
|
|
31
|
+
weighted: { weight: 100n, tolerance: 0n },
|
|
32
|
+
bridge: ethers.constants.AddressZero,
|
|
33
|
+
bridgeLockTime: 1,
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
testLogger,
|
|
37
|
+
),
|
|
38
|
+
).to.throw('At least two chains must be configured');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should throw an error when weight is negative', () => {
|
|
42
|
+
expect(
|
|
43
|
+
() =>
|
|
44
|
+
new WeightedStrategy(
|
|
45
|
+
{
|
|
46
|
+
[chain1]: {
|
|
47
|
+
weighted: { weight: 100n, tolerance: 0n },
|
|
48
|
+
bridge: ethers.constants.AddressZero,
|
|
49
|
+
bridgeLockTime: 1,
|
|
50
|
+
},
|
|
51
|
+
[chain2]: {
|
|
52
|
+
weighted: { weight: -1n, tolerance: 0n },
|
|
53
|
+
bridge: ethers.constants.AddressZero,
|
|
54
|
+
bridgeLockTime: 1,
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
testLogger,
|
|
58
|
+
),
|
|
59
|
+
).to.throw('Weight (-1) must not be negative for chain2');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should throw an error when the total weight is 0', () => {
|
|
63
|
+
expect(
|
|
64
|
+
() =>
|
|
65
|
+
new WeightedStrategy(
|
|
66
|
+
{
|
|
67
|
+
[chain1]: {
|
|
68
|
+
weighted: { weight: 0n, tolerance: 0n },
|
|
69
|
+
bridge: ethers.constants.AddressZero,
|
|
70
|
+
bridgeLockTime: 1,
|
|
71
|
+
},
|
|
72
|
+
[chain2]: {
|
|
73
|
+
weighted: { weight: 0n, tolerance: 0n },
|
|
74
|
+
bridge: ethers.constants.AddressZero,
|
|
75
|
+
bridgeLockTime: 1,
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
testLogger,
|
|
79
|
+
),
|
|
80
|
+
).to.throw('The total weight for all chains must be greater than 0');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should throw an error when tolerance is less than 0 or greater than 100', () => {
|
|
84
|
+
expect(
|
|
85
|
+
() =>
|
|
86
|
+
new WeightedStrategy(
|
|
87
|
+
{
|
|
88
|
+
[chain1]: {
|
|
89
|
+
weighted: { weight: 100n, tolerance: 0n },
|
|
90
|
+
bridge: ethers.constants.AddressZero,
|
|
91
|
+
bridgeLockTime: 1,
|
|
92
|
+
},
|
|
93
|
+
[chain2]: {
|
|
94
|
+
weighted: { weight: 100n, tolerance: -1n },
|
|
95
|
+
bridge: ethers.constants.AddressZero,
|
|
96
|
+
bridgeLockTime: 1,
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
testLogger,
|
|
100
|
+
),
|
|
101
|
+
).to.throw('Tolerance (-1) must be between 0 and 100 for chain2');
|
|
102
|
+
|
|
103
|
+
expect(
|
|
104
|
+
() =>
|
|
105
|
+
new WeightedStrategy(
|
|
106
|
+
{
|
|
107
|
+
[chain1]: {
|
|
108
|
+
weighted: { weight: 100n, tolerance: 100n },
|
|
109
|
+
bridge: ethers.constants.AddressZero,
|
|
110
|
+
bridgeLockTime: 1,
|
|
111
|
+
},
|
|
112
|
+
[chain2]: {
|
|
113
|
+
weighted: { weight: 100n, tolerance: 101n },
|
|
114
|
+
bridge: ethers.constants.AddressZero,
|
|
115
|
+
bridgeLockTime: 1,
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
testLogger,
|
|
119
|
+
),
|
|
120
|
+
).to.throw('Tolerance (101) must be between 0 and 100 for chain2');
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe('getRebalancingRoutes', () => {
|
|
125
|
+
it('should throw an error when raw balances chains length does not match configured chains length', () => {
|
|
126
|
+
expect(() =>
|
|
127
|
+
new WeightedStrategy(
|
|
128
|
+
{
|
|
129
|
+
[chain1]: {
|
|
130
|
+
weighted: { weight: 100n, tolerance: 0n },
|
|
131
|
+
bridge: ethers.constants.AddressZero,
|
|
132
|
+
bridgeLockTime: 1,
|
|
133
|
+
},
|
|
134
|
+
[chain2]: {
|
|
135
|
+
weighted: { weight: 100n, tolerance: 0n },
|
|
136
|
+
bridge: ethers.constants.AddressZero,
|
|
137
|
+
bridgeLockTime: 1,
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
testLogger,
|
|
141
|
+
).getRebalancingRoutes({
|
|
142
|
+
[chain1]: ethers.utils.parseEther('100').toBigInt(),
|
|
143
|
+
[chain2]: ethers.utils.parseEther('200').toBigInt(),
|
|
144
|
+
[chain3]: ethers.utils.parseEther('300').toBigInt(),
|
|
145
|
+
}),
|
|
146
|
+
).to.throw('Config chains do not match raw balances chains length');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should throw an error when a raw balance is missing', () => {
|
|
150
|
+
expect(() =>
|
|
151
|
+
new WeightedStrategy(
|
|
152
|
+
{
|
|
153
|
+
[chain1]: {
|
|
154
|
+
weighted: { weight: 100n, tolerance: 0n },
|
|
155
|
+
bridge: ethers.constants.AddressZero,
|
|
156
|
+
bridgeLockTime: 1,
|
|
157
|
+
},
|
|
158
|
+
[chain2]: {
|
|
159
|
+
weighted: { weight: 100n, tolerance: 0n },
|
|
160
|
+
bridge: ethers.constants.AddressZero,
|
|
161
|
+
bridgeLockTime: 1,
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
testLogger,
|
|
165
|
+
).getRebalancingRoutes({
|
|
166
|
+
[chain1]: ethers.utils.parseEther('100').toBigInt(),
|
|
167
|
+
[chain3]: ethers.utils.parseEther('300').toBigInt(),
|
|
168
|
+
} as RawBalances),
|
|
169
|
+
).to.throw('Raw balance for chain chain2 not found');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('should throw an error when a raw balance is negative', () => {
|
|
173
|
+
expect(() =>
|
|
174
|
+
new WeightedStrategy(
|
|
175
|
+
{
|
|
176
|
+
[chain1]: {
|
|
177
|
+
weighted: { weight: 100n, tolerance: 0n },
|
|
178
|
+
bridge: ethers.constants.AddressZero,
|
|
179
|
+
bridgeLockTime: 1,
|
|
180
|
+
},
|
|
181
|
+
[chain2]: {
|
|
182
|
+
weighted: { weight: 100n, tolerance: 0n },
|
|
183
|
+
bridge: ethers.constants.AddressZero,
|
|
184
|
+
bridgeLockTime: 1,
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
testLogger,
|
|
188
|
+
).getRebalancingRoutes({
|
|
189
|
+
[chain1]: ethers.utils.parseEther('100').toBigInt(),
|
|
190
|
+
[chain2]: ethers.utils.parseEther('-200').toBigInt(),
|
|
191
|
+
}),
|
|
192
|
+
).to.throw('Raw balance for chain chain2 is negative');
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('should return an empty array when all chains are balanced', () => {
|
|
196
|
+
const strategy = new WeightedStrategy(
|
|
197
|
+
{
|
|
198
|
+
[chain1]: {
|
|
199
|
+
weighted: { weight: 100n, tolerance: 0n },
|
|
200
|
+
bridge: ethers.constants.AddressZero,
|
|
201
|
+
bridgeLockTime: 1,
|
|
202
|
+
},
|
|
203
|
+
[chain2]: {
|
|
204
|
+
weighted: { weight: 100n, tolerance: 0n },
|
|
205
|
+
bridge: ethers.constants.AddressZero,
|
|
206
|
+
bridgeLockTime: 1,
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
testLogger,
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
const rawBalances = {
|
|
213
|
+
[chain1]: ethers.utils.parseEther('100').toBigInt(),
|
|
214
|
+
[chain2]: ethers.utils.parseEther('100').toBigInt(),
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const routes = strategy.getRebalancingRoutes(rawBalances);
|
|
218
|
+
|
|
219
|
+
expect(routes).to.be.empty;
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('should return a single route when a chain is unbalanced', () => {
|
|
223
|
+
const strategy = new WeightedStrategy(
|
|
224
|
+
{
|
|
225
|
+
[chain1]: {
|
|
226
|
+
weighted: { weight: 100n, tolerance: 0n },
|
|
227
|
+
bridge: ethers.constants.AddressZero,
|
|
228
|
+
bridgeLockTime: 1,
|
|
229
|
+
},
|
|
230
|
+
[chain2]: {
|
|
231
|
+
weighted: { weight: 100n, tolerance: 0n },
|
|
232
|
+
bridge: ethers.constants.AddressZero,
|
|
233
|
+
bridgeLockTime: 1,
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
testLogger,
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
const rawBalances = {
|
|
240
|
+
[chain1]: ethers.utils.parseEther('100').toBigInt(),
|
|
241
|
+
[chain2]: ethers.utils.parseEther('200').toBigInt(),
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const routes = strategy.getRebalancingRoutes(rawBalances);
|
|
245
|
+
|
|
246
|
+
expect(routes).to.deep.equal([
|
|
247
|
+
{
|
|
248
|
+
origin: chain2,
|
|
249
|
+
destination: chain1,
|
|
250
|
+
amount: ethers.utils.parseEther('50').toBigInt(),
|
|
251
|
+
},
|
|
252
|
+
]);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('should return an empty array when a chain is unbalanced but has tolerance', () => {
|
|
256
|
+
const strategy = new WeightedStrategy(
|
|
257
|
+
{
|
|
258
|
+
[chain1]: {
|
|
259
|
+
weighted: { weight: 100n, tolerance: 1n },
|
|
260
|
+
bridge: ethers.constants.AddressZero,
|
|
261
|
+
bridgeLockTime: 1,
|
|
262
|
+
},
|
|
263
|
+
[chain2]: {
|
|
264
|
+
weighted: { weight: 100n, tolerance: 1n },
|
|
265
|
+
bridge: ethers.constants.AddressZero,
|
|
266
|
+
bridgeLockTime: 1,
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
testLogger,
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
const rawBalances = {
|
|
273
|
+
[chain1]: ethers.utils.parseEther('100').toBigInt(),
|
|
274
|
+
[chain2]: ethers.utils.parseEther('101').toBigInt(),
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
const routes = strategy.getRebalancingRoutes(rawBalances);
|
|
278
|
+
|
|
279
|
+
expect(routes).to.be.empty;
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('should return a single route when two chains are unbalanced and can be solved with a single transfer', () => {
|
|
283
|
+
const strategy = new WeightedStrategy(
|
|
284
|
+
{
|
|
285
|
+
[chain1]: {
|
|
286
|
+
weighted: { weight: 100n, tolerance: 0n },
|
|
287
|
+
bridge: ethers.constants.AddressZero,
|
|
288
|
+
bridgeLockTime: 1,
|
|
289
|
+
},
|
|
290
|
+
[chain2]: {
|
|
291
|
+
weighted: { weight: 100n, tolerance: 0n },
|
|
292
|
+
bridge: ethers.constants.AddressZero,
|
|
293
|
+
bridgeLockTime: 1,
|
|
294
|
+
},
|
|
295
|
+
[chain3]: {
|
|
296
|
+
weighted: { weight: 100n, tolerance: 0n },
|
|
297
|
+
bridge: ethers.constants.AddressZero,
|
|
298
|
+
bridgeLockTime: 1,
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
testLogger,
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
const rawBalances = {
|
|
305
|
+
[chain1]: ethers.utils.parseEther('100').toBigInt(),
|
|
306
|
+
[chain2]: ethers.utils.parseEther('200').toBigInt(),
|
|
307
|
+
[chain3]: ethers.utils.parseEther('300').toBigInt(),
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
const routes = strategy.getRebalancingRoutes(rawBalances);
|
|
311
|
+
|
|
312
|
+
expect(routes).to.deep.equal([
|
|
313
|
+
{
|
|
314
|
+
origin: chain3,
|
|
315
|
+
destination: chain1,
|
|
316
|
+
amount: ethers.utils.parseEther('100').toBigInt(),
|
|
317
|
+
},
|
|
318
|
+
]);
|
|
319
|
+
});
|
|
320
|
+
it('should return two routes when two chains are unbalanced and cannot be solved with a single transfer', () => {
|
|
321
|
+
const strategy = new WeightedStrategy(
|
|
322
|
+
{
|
|
323
|
+
[chain1]: {
|
|
324
|
+
weighted: { weight: 100n, tolerance: 0n },
|
|
325
|
+
bridge: ethers.constants.AddressZero,
|
|
326
|
+
bridgeLockTime: 1,
|
|
327
|
+
},
|
|
328
|
+
[chain2]: {
|
|
329
|
+
weighted: { weight: 100n, tolerance: 0n },
|
|
330
|
+
bridge: ethers.constants.AddressZero,
|
|
331
|
+
bridgeLockTime: 1,
|
|
332
|
+
},
|
|
333
|
+
[chain3]: {
|
|
334
|
+
weighted: { weight: 100n, tolerance: 0n },
|
|
335
|
+
bridge: ethers.constants.AddressZero,
|
|
336
|
+
bridgeLockTime: 1,
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
testLogger,
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
const rawBalances = {
|
|
343
|
+
[chain1]: ethers.utils.parseEther('100').toBigInt(),
|
|
344
|
+
[chain2]: ethers.utils.parseEther('100').toBigInt(),
|
|
345
|
+
[chain3]: ethers.utils.parseEther('500').toBigInt(),
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
const routes = strategy.getRebalancingRoutes(rawBalances);
|
|
349
|
+
|
|
350
|
+
expect(routes).to.deep.equal([
|
|
351
|
+
{
|
|
352
|
+
origin: chain3,
|
|
353
|
+
destination: chain1,
|
|
354
|
+
amount: 133333333333333333333n,
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
origin: chain3,
|
|
358
|
+
destination: chain2,
|
|
359
|
+
amount: 133333333333333333333n,
|
|
360
|
+
},
|
|
361
|
+
]);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it('should return routes to balance different weighted chains', () => {
|
|
365
|
+
const strategy = new WeightedStrategy(
|
|
366
|
+
{
|
|
367
|
+
[chain1]: {
|
|
368
|
+
weighted: { weight: 50n, tolerance: 0n },
|
|
369
|
+
bridge: ethers.constants.AddressZero,
|
|
370
|
+
bridgeLockTime: 1,
|
|
371
|
+
},
|
|
372
|
+
[chain2]: {
|
|
373
|
+
weighted: { weight: 25n, tolerance: 0n },
|
|
374
|
+
bridge: ethers.constants.AddressZero,
|
|
375
|
+
bridgeLockTime: 1,
|
|
376
|
+
},
|
|
377
|
+
[chain3]: {
|
|
378
|
+
weighted: { weight: 25n, tolerance: 0n },
|
|
379
|
+
bridge: ethers.constants.AddressZero,
|
|
380
|
+
bridgeLockTime: 1,
|
|
381
|
+
},
|
|
382
|
+
},
|
|
383
|
+
testLogger,
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
const rawBalances = {
|
|
387
|
+
[chain1]: ethers.utils.parseEther('100').toBigInt(),
|
|
388
|
+
[chain2]: ethers.utils.parseEther('100').toBigInt(),
|
|
389
|
+
[chain3]: ethers.utils.parseEther('100').toBigInt(),
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
const routes = strategy.getRebalancingRoutes(rawBalances);
|
|
393
|
+
|
|
394
|
+
expect(routes).to.deep.equal([
|
|
395
|
+
{
|
|
396
|
+
origin: chain2,
|
|
397
|
+
destination: chain1,
|
|
398
|
+
amount: ethers.utils.parseEther('25').toBigInt(),
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
origin: chain3,
|
|
402
|
+
destination: chain1,
|
|
403
|
+
amount: ethers.utils.parseEther('25').toBigInt(),
|
|
404
|
+
},
|
|
405
|
+
]);
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { Logger } from 'pino';
|
|
2
|
+
|
|
3
|
+
import type { WeightedStrategyConfig } from '@hyperlane-xyz/sdk';
|
|
4
|
+
|
|
5
|
+
import type { RawBalances } from '../interfaces/IStrategy.js';
|
|
6
|
+
import { Metrics } from '../metrics/Metrics.js';
|
|
7
|
+
|
|
8
|
+
import { BaseStrategy, type Delta } from './BaseStrategy.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Strategy implementation that rebalance based on weights
|
|
12
|
+
* It distributes funds across chains based on their weights
|
|
13
|
+
*/
|
|
14
|
+
export class WeightedStrategy extends BaseStrategy {
|
|
15
|
+
private readonly config: WeightedStrategyConfig;
|
|
16
|
+
private readonly totalWeight: bigint;
|
|
17
|
+
protected readonly logger: Logger;
|
|
18
|
+
|
|
19
|
+
constructor(
|
|
20
|
+
config: WeightedStrategyConfig,
|
|
21
|
+
logger: Logger,
|
|
22
|
+
metrics?: Metrics,
|
|
23
|
+
) {
|
|
24
|
+
const chains = Object.keys(config);
|
|
25
|
+
const log = logger.child({ class: WeightedStrategy.name });
|
|
26
|
+
super(chains, log, metrics);
|
|
27
|
+
this.logger = log;
|
|
28
|
+
|
|
29
|
+
let totalWeight = 0n;
|
|
30
|
+
|
|
31
|
+
for (const chain of chains) {
|
|
32
|
+
const { weight, tolerance } = config[chain].weighted;
|
|
33
|
+
|
|
34
|
+
if (weight < 0n) {
|
|
35
|
+
throw new Error(`Weight (${weight}) must not be negative for ${chain}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (tolerance < 0n || tolerance > 100n) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
`Tolerance (${tolerance}) must be between 0 and 100 for ${chain}`,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
totalWeight += weight;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (totalWeight <= 0n) {
|
|
48
|
+
throw new Error('The total weight for all chains must be greater than 0');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
this.config = config;
|
|
52
|
+
this.totalWeight = totalWeight;
|
|
53
|
+
this.logger.info('WeightedStrategy created');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Gets balances categorized by surplus and deficit based on weights
|
|
58
|
+
*/
|
|
59
|
+
protected getCategorizedBalances(rawBalances: RawBalances): {
|
|
60
|
+
surpluses: Delta[];
|
|
61
|
+
deficits: Delta[];
|
|
62
|
+
} {
|
|
63
|
+
// Get the total balance from all chains
|
|
64
|
+
const total = this.chains.reduce(
|
|
65
|
+
(sum, chain) => sum + rawBalances[chain],
|
|
66
|
+
0n,
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
return this.chains.reduce(
|
|
70
|
+
(acc, chain) => {
|
|
71
|
+
const { weight, tolerance } = this.config[chain].weighted;
|
|
72
|
+
const target = (total * weight) / this.totalWeight;
|
|
73
|
+
const toleranceAmount = (target * tolerance) / 100n;
|
|
74
|
+
const balance = rawBalances[chain];
|
|
75
|
+
|
|
76
|
+
// Apply the tolerance to deficits to prevent small imbalances
|
|
77
|
+
if (balance < target - toleranceAmount) {
|
|
78
|
+
acc.deficits.push({ chain, amount: target - balance });
|
|
79
|
+
} else if (balance > target) {
|
|
80
|
+
acc.surpluses.push({ chain, amount: balance - target });
|
|
81
|
+
} else {
|
|
82
|
+
// Do nothing as the balance is already on target
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return acc;
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
surpluses: [] as Delta[],
|
|
89
|
+
deficits: [] as Delta[],
|
|
90
|
+
},
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { ethers } from 'ethers';
|
|
2
|
+
|
|
3
|
+
import { RebalancerStrategyOptions } 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
|
+
|
|
9
|
+
export class MockRebalancer implements IRebalancer {
|
|
10
|
+
rebalance(_routes: RebalancingRoute[]): Promise<void> {
|
|
11
|
+
return Promise.resolve();
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function buildTestConfig(
|
|
16
|
+
overrides: Partial<RebalancerConfig> = {},
|
|
17
|
+
chains: string[] = ['chain1'],
|
|
18
|
+
): RebalancerConfig {
|
|
19
|
+
const baseChains = chains.reduce(
|
|
20
|
+
(acc, chain) => {
|
|
21
|
+
(acc as any)[chain] = {
|
|
22
|
+
bridgeLockTime: 60 * 1000,
|
|
23
|
+
bridge: ethers.constants.AddressZero,
|
|
24
|
+
weighted: {
|
|
25
|
+
weight: BigInt(1),
|
|
26
|
+
tolerance: BigInt(0),
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
return acc;
|
|
30
|
+
},
|
|
31
|
+
{} as Record<string, any>,
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
warpRouteId: 'test-route',
|
|
36
|
+
strategyConfig: {
|
|
37
|
+
rebalanceStrategy: RebalancerStrategyOptions.Weighted,
|
|
38
|
+
chains: {
|
|
39
|
+
...baseChains,
|
|
40
|
+
...(overrides.strategyConfig?.chains ?? {}),
|
|
41
|
+
},
|
|
42
|
+
...overrides.strategyConfig,
|
|
43
|
+
},
|
|
44
|
+
...overrides,
|
|
45
|
+
} as any as RebalancerConfig;
|
|
46
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { Logger } from 'pino';
|
|
2
|
+
|
|
3
|
+
export type InflightRebalanceQueryParams = {
|
|
4
|
+
bridges: string[];
|
|
5
|
+
domains: number[];
|
|
6
|
+
txSender: string;
|
|
7
|
+
limit?: number;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export class ExplorerClient {
|
|
11
|
+
constructor(private readonly baseUrl: string) {}
|
|
12
|
+
|
|
13
|
+
private toBytea(addr: string): string {
|
|
14
|
+
return addr.replace(/^0x/i, '\\x').toLowerCase();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async hasUndeliveredRebalance(
|
|
18
|
+
params: InflightRebalanceQueryParams,
|
|
19
|
+
logger: Logger,
|
|
20
|
+
): Promise<boolean> {
|
|
21
|
+
const { bridges, domains, txSender, limit = 5 } = params;
|
|
22
|
+
|
|
23
|
+
const variables = {
|
|
24
|
+
senders: bridges.map((a) => this.toBytea(a)),
|
|
25
|
+
recipients: bridges.map((a) => this.toBytea(a)),
|
|
26
|
+
originDomains: domains,
|
|
27
|
+
destDomains: domains,
|
|
28
|
+
txSenders: [this.toBytea(txSender)],
|
|
29
|
+
limit,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
logger.debug({ variables }, 'Explorer query variables');
|
|
33
|
+
|
|
34
|
+
const query = `
|
|
35
|
+
query InflightRebalancesForRoute(
|
|
36
|
+
$senders: [bytea!],
|
|
37
|
+
$recipients: [bytea!],
|
|
38
|
+
$originDomains: [Int!],
|
|
39
|
+
$destDomains: [Int!],
|
|
40
|
+
$txSenders: [bytea!],
|
|
41
|
+
$limit: Int = 25
|
|
42
|
+
) {
|
|
43
|
+
message_view(
|
|
44
|
+
where: {
|
|
45
|
+
_and: [
|
|
46
|
+
{ is_delivered: { _eq: false } },
|
|
47
|
+
{ sender: { _in: $senders } },
|
|
48
|
+
{ recipient: { _in: $recipients } },
|
|
49
|
+
{ origin_domain_id: { _in: $originDomains } },
|
|
50
|
+
{ destination_domain_id: { _in: $destDomains } },
|
|
51
|
+
{ origin_tx_sender: { _in: $txSenders } }
|
|
52
|
+
]
|
|
53
|
+
}
|
|
54
|
+
order_by: { origin_tx_id: desc }
|
|
55
|
+
limit: $limit
|
|
56
|
+
) {
|
|
57
|
+
msg_id
|
|
58
|
+
origin_domain_id
|
|
59
|
+
destination_domain_id
|
|
60
|
+
sender
|
|
61
|
+
recipient
|
|
62
|
+
origin_tx_hash
|
|
63
|
+
origin_tx_sender
|
|
64
|
+
is_delivered
|
|
65
|
+
}
|
|
66
|
+
}`;
|
|
67
|
+
|
|
68
|
+
const res = await fetch(this.baseUrl, {
|
|
69
|
+
method: 'POST',
|
|
70
|
+
headers: { 'Content-Type': 'application/json' },
|
|
71
|
+
body: JSON.stringify({ query, variables }),
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
logger.debug({ status: res.status }, 'Explorer query response');
|
|
75
|
+
|
|
76
|
+
if (!res.ok) {
|
|
77
|
+
let errorDetails: string;
|
|
78
|
+
try {
|
|
79
|
+
const errorJson = await res.json();
|
|
80
|
+
errorDetails = JSON.stringify(errorJson);
|
|
81
|
+
} catch (_e) {
|
|
82
|
+
try {
|
|
83
|
+
// Fallback to text if JSON parsing fails
|
|
84
|
+
errorDetails = await res.text();
|
|
85
|
+
} catch (_textError) {
|
|
86
|
+
errorDetails = 'Unable to read response body';
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
throw new Error(`Explorer query failed: ${res.status} ${errorDetails}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const json = await res.json();
|
|
93
|
+
const rows = json?.data?.message_view ?? [];
|
|
94
|
+
|
|
95
|
+
logger.debug({ rows }, 'Explorer query rows');
|
|
96
|
+
|
|
97
|
+
return rows.length > 0;
|
|
98
|
+
}
|
|
99
|
+
}
|