@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,390 @@
|
|
|
1
|
+
import { Logger } from 'pino';
|
|
2
|
+
|
|
3
|
+
import { type ChainMap, type ChainName, type Token } from '@hyperlane-xyz/sdk';
|
|
4
|
+
import { toWei } from '@hyperlane-xyz/utils';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
type CollateralDeficitStrategyConfig,
|
|
8
|
+
RebalancerStrategyOptions,
|
|
9
|
+
} from '../config/types.js';
|
|
10
|
+
import type {
|
|
11
|
+
InflightContext,
|
|
12
|
+
RawBalances,
|
|
13
|
+
Route,
|
|
14
|
+
StrategyRoute,
|
|
15
|
+
} from '../interfaces/IStrategy.js';
|
|
16
|
+
import { Metrics } from '../metrics/Metrics.js';
|
|
17
|
+
import type { BridgeConfigWithOverride } from '../utils/bridgeUtils.js';
|
|
18
|
+
|
|
19
|
+
import { BaseStrategy, type Delta } from './BaseStrategy.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Strategy that detects collateral deficits (negative effective balances)
|
|
23
|
+
* and proposes JIT rebalances using fast bridges.
|
|
24
|
+
*
|
|
25
|
+
* Logic:
|
|
26
|
+
* 1. Filter pendingRebalances to only those using this strategy's configured bridges
|
|
27
|
+
* 2. Simulate filtered pending rebalances to get projected balances
|
|
28
|
+
* 3. Negative simulated balance = deficit (magnitude + buffer)
|
|
29
|
+
* 4. Positive simulated balance = potential surplus
|
|
30
|
+
*/
|
|
31
|
+
export class CollateralDeficitStrategy extends BaseStrategy {
|
|
32
|
+
readonly name = RebalancerStrategyOptions.CollateralDeficit;
|
|
33
|
+
private readonly config: CollateralDeficitStrategyConfig;
|
|
34
|
+
protected readonly logger: Logger;
|
|
35
|
+
|
|
36
|
+
constructor(
|
|
37
|
+
config: CollateralDeficitStrategyConfig,
|
|
38
|
+
tokensByChainName: ChainMap<Token>,
|
|
39
|
+
logger: Logger,
|
|
40
|
+
bridgeConfigs: ChainMap<BridgeConfigWithOverride>,
|
|
41
|
+
metrics?: Metrics,
|
|
42
|
+
) {
|
|
43
|
+
const chains = Object.keys(config);
|
|
44
|
+
const log = logger.child({ class: CollateralDeficitStrategy.name });
|
|
45
|
+
super(chains, log, bridgeConfigs, metrics, tokensByChainName);
|
|
46
|
+
this.logger = log;
|
|
47
|
+
this.config = config;
|
|
48
|
+
this.logger.info('CollateralDeficitStrategy created');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Categorizes balances into surpluses and deficits.
|
|
53
|
+
*
|
|
54
|
+
* 1. Filter pendingRebalances/proposedRebalances by configured bridges
|
|
55
|
+
* 2. Simulate those rebalances to get projected balances
|
|
56
|
+
* 3. Negative balance = deficit (magnitude + buffer)
|
|
57
|
+
* 4. Positive balance = potential surplus
|
|
58
|
+
*/
|
|
59
|
+
protected getCategorizedBalances(
|
|
60
|
+
rawBalances: RawBalances,
|
|
61
|
+
pendingRebalances?: Route[],
|
|
62
|
+
proposedRebalances?: StrategyRoute[],
|
|
63
|
+
): {
|
|
64
|
+
surpluses: Delta[];
|
|
65
|
+
deficits: Delta[];
|
|
66
|
+
} {
|
|
67
|
+
// Filter pending rebalances to only those using this strategy's bridges
|
|
68
|
+
const filteredPending = this.filterByConfiguredBridges(pendingRebalances);
|
|
69
|
+
const filteredProposed = this.filterByConfiguredBridges(proposedRebalances);
|
|
70
|
+
|
|
71
|
+
this.logger.debug(
|
|
72
|
+
{
|
|
73
|
+
context: this.constructor.name,
|
|
74
|
+
totalPending: pendingRebalances?.length ?? 0,
|
|
75
|
+
filteredPending: filteredPending.length,
|
|
76
|
+
totalProposed: proposedRebalances?.length ?? 0,
|
|
77
|
+
filteredProposed: filteredProposed.length,
|
|
78
|
+
},
|
|
79
|
+
'Filtered rebalances by configured bridges',
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
// Step 1: Simulate pending rebalances (in-flight, origin already deducted on-chain)
|
|
83
|
+
let simulatedBalances = this.simulatePendingRebalances(
|
|
84
|
+
rawBalances,
|
|
85
|
+
filteredPending,
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
// Step 2: Simulate proposed rebalances (from earlier strategies, not yet executed)
|
|
89
|
+
simulatedBalances = this.simulateProposedRebalances(
|
|
90
|
+
simulatedBalances,
|
|
91
|
+
filteredProposed,
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const surpluses: Delta[] = [];
|
|
95
|
+
const deficits: Delta[] = [];
|
|
96
|
+
|
|
97
|
+
for (const chain of this.chains) {
|
|
98
|
+
const balance = simulatedBalances[chain];
|
|
99
|
+
const token = this.getTokenByChainName(chain);
|
|
100
|
+
const bufferWei = BigInt(
|
|
101
|
+
toWei(this.config[chain].buffer, token.decimals),
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
if (balance < 0n) {
|
|
105
|
+
// Negative balance indicates deficit
|
|
106
|
+
const deficitAmount = -balance + bufferWei;
|
|
107
|
+
deficits.push({ chain, amount: deficitAmount });
|
|
108
|
+
|
|
109
|
+
this.logger.debug(
|
|
110
|
+
{
|
|
111
|
+
context: this.constructor.name,
|
|
112
|
+
chain,
|
|
113
|
+
simulatedBalance: balance.toString(),
|
|
114
|
+
buffer: bufferWei.toString(),
|
|
115
|
+
deficitAmount: deficitAmount.toString(),
|
|
116
|
+
},
|
|
117
|
+
'Detected collateral deficit',
|
|
118
|
+
);
|
|
119
|
+
} else if (balance > 0n) {
|
|
120
|
+
// Positive balance is potential surplus
|
|
121
|
+
surpluses.push({ chain, amount: balance });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
this.logger.info(
|
|
126
|
+
{
|
|
127
|
+
surpluses: surpluses.map((s) => ({
|
|
128
|
+
chain: s.chain,
|
|
129
|
+
amount: s.amount.toString(),
|
|
130
|
+
})),
|
|
131
|
+
deficits: deficits.map((d) => ({
|
|
132
|
+
chain: d.chain,
|
|
133
|
+
amount: d.amount.toString(),
|
|
134
|
+
})),
|
|
135
|
+
},
|
|
136
|
+
'Balance categorization',
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
return { surpluses, deficits };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Override to prefer transfer origins when selecting surplus chains.
|
|
144
|
+
*
|
|
145
|
+
* When a user transfer creates a deficit, the origin chain of that transfer
|
|
146
|
+
* is the natural source of funds (user deposited there). This prevents
|
|
147
|
+
* unnecessarily draining the largest balance (typically ethereum at 70%).
|
|
148
|
+
*/
|
|
149
|
+
override getRebalancingRoutes(
|
|
150
|
+
rawBalances: RawBalances,
|
|
151
|
+
inflightContext?: InflightContext,
|
|
152
|
+
): StrategyRoute[] {
|
|
153
|
+
const pendingRebalances = inflightContext?.pendingRebalances ?? [];
|
|
154
|
+
const pendingTransfers = inflightContext?.pendingTransfers ?? [];
|
|
155
|
+
|
|
156
|
+
this.logger.info(
|
|
157
|
+
{
|
|
158
|
+
strategy: this.name,
|
|
159
|
+
balances: Object.entries(rawBalances).map(([c, b]) => ({
|
|
160
|
+
chain: c,
|
|
161
|
+
balance: b.toString(),
|
|
162
|
+
})),
|
|
163
|
+
pendingRebalances: pendingRebalances.length,
|
|
164
|
+
pendingTransfers: pendingTransfers.length,
|
|
165
|
+
},
|
|
166
|
+
'Strategy evaluating',
|
|
167
|
+
);
|
|
168
|
+
this.validateRawBalances(rawBalances);
|
|
169
|
+
|
|
170
|
+
const actualBalances = rawBalances;
|
|
171
|
+
|
|
172
|
+
// Step 1: Reserve collateral for pending user transfers
|
|
173
|
+
const effectiveBalances = this.reserveCollateral(
|
|
174
|
+
rawBalances,
|
|
175
|
+
pendingTransfers,
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
// Step 2: Get categorized balances
|
|
179
|
+
const { surpluses, deficits } = this.getCategorizedBalances(
|
|
180
|
+
effectiveBalances,
|
|
181
|
+
pendingRebalances,
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
this.logger.debug(
|
|
185
|
+
{ context: this.constructor.name, surpluses },
|
|
186
|
+
'Surpluses calculated',
|
|
187
|
+
);
|
|
188
|
+
this.logger.debug(
|
|
189
|
+
{ context: this.constructor.name, deficits },
|
|
190
|
+
'Deficits calculated',
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
const totalSurplus = surpluses.reduce((sum, s) => sum + s.amount, 0n);
|
|
194
|
+
const totalDeficit = deficits.reduce((sum, d) => sum + d.amount, 0n);
|
|
195
|
+
|
|
196
|
+
this.logger.debug(
|
|
197
|
+
{ context: this.constructor.name, totalSurplus: totalSurplus.toString() },
|
|
198
|
+
'Total surplus calculated',
|
|
199
|
+
);
|
|
200
|
+
this.logger.debug(
|
|
201
|
+
{ context: this.constructor.name, totalDeficit: totalDeficit.toString() },
|
|
202
|
+
'Total deficit calculated',
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
// Scale deficits if needed
|
|
206
|
+
if (totalSurplus < totalDeficit) {
|
|
207
|
+
this.logger.warn(
|
|
208
|
+
{
|
|
209
|
+
context: this.constructor.name,
|
|
210
|
+
totalSurplus: totalSurplus.toString(),
|
|
211
|
+
totalDeficit: totalDeficit.toString(),
|
|
212
|
+
},
|
|
213
|
+
'Deficits are greater than surpluses. Scaling deficits',
|
|
214
|
+
);
|
|
215
|
+
this.metrics?.recordRebalancerFailure();
|
|
216
|
+
for (const deficit of deficits) {
|
|
217
|
+
deficit.amount = (deficit.amount * totalSurplus) / totalDeficit;
|
|
218
|
+
}
|
|
219
|
+
this.logger.debug(
|
|
220
|
+
{ context: this.constructor.name, deficits },
|
|
221
|
+
'Scaled deficits',
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Build transfer origin map for deficit chains
|
|
226
|
+
const deficitChains = new Set(deficits.map((d) => d.chain));
|
|
227
|
+
const transferOriginMap = this.buildTransferOriginMap(
|
|
228
|
+
pendingTransfers,
|
|
229
|
+
deficitChains,
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
// Sort surpluses with transfer origin preference (KEY CHANGE from base class)
|
|
233
|
+
this.sortSurplusesWithOriginPreference(surpluses, transferOriginMap);
|
|
234
|
+
|
|
235
|
+
// Sort deficits by amount (largest first)
|
|
236
|
+
deficits.sort((a, b) => (a.amount > b.amount ? -1 : 1));
|
|
237
|
+
|
|
238
|
+
const routes: StrategyRoute[] = [];
|
|
239
|
+
|
|
240
|
+
// Match surpluses to deficits
|
|
241
|
+
while (deficits.length > 0 && surpluses.length > 0) {
|
|
242
|
+
const surplus = surpluses[0];
|
|
243
|
+
const deficit = deficits[0];
|
|
244
|
+
const transferAmount =
|
|
245
|
+
surplus.amount > deficit.amount ? deficit.amount : surplus.amount;
|
|
246
|
+
|
|
247
|
+
if (transferAmount > 0n) {
|
|
248
|
+
const bridgeConfig = this.getBridgeConfigForRoute(
|
|
249
|
+
surplus.chain,
|
|
250
|
+
deficit.chain,
|
|
251
|
+
);
|
|
252
|
+
routes.push({
|
|
253
|
+
origin: surplus.chain,
|
|
254
|
+
destination: deficit.chain,
|
|
255
|
+
amount: transferAmount,
|
|
256
|
+
bridge: bridgeConfig.bridge,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
deficit.amount -= transferAmount;
|
|
261
|
+
surplus.amount -= transferAmount;
|
|
262
|
+
|
|
263
|
+
if (deficit.amount <= 0n) deficits.shift();
|
|
264
|
+
if (surplus.amount <= 0n) surpluses.shift();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
this.logger.debug(
|
|
268
|
+
{ context: this.constructor.name, routes },
|
|
269
|
+
'Generated routes',
|
|
270
|
+
);
|
|
271
|
+
this.logger.info(
|
|
272
|
+
{ context: this.constructor.name, numberOfRoutes: routes.length },
|
|
273
|
+
'Found rebalancing routes',
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
const filteredRoutes = this.filterRoutes(routes, actualBalances);
|
|
277
|
+
|
|
278
|
+
this.logger.debug(
|
|
279
|
+
{
|
|
280
|
+
context: this.constructor.name,
|
|
281
|
+
filteredRoutesCount: filteredRoutes.length,
|
|
282
|
+
droppedCount: routes.length - filteredRoutes.length,
|
|
283
|
+
},
|
|
284
|
+
'Filtered rebalancing routes',
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
return filteredRoutes;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Filter pending rebalances to only those using this strategy's configured bridges.
|
|
292
|
+
* A rebalance matches if:
|
|
293
|
+
* - Its bridge matches the configured bridge (with overrides) for the route, OR
|
|
294
|
+
* - It has no bridge (recovered from Explorer, can't verify - include to be safe)
|
|
295
|
+
*/
|
|
296
|
+
private filterByConfiguredBridges(pendingRebalances?: Route[]): Route[] {
|
|
297
|
+
if (!pendingRebalances || pendingRebalances.length === 0) {
|
|
298
|
+
return [];
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return pendingRebalances.filter((rebalance) => {
|
|
302
|
+
if (!('bridge' in rebalance) || !rebalance.bridge) {
|
|
303
|
+
this.logger.debug(
|
|
304
|
+
{ origin: rebalance.origin, destination: rebalance.destination },
|
|
305
|
+
'Including pending rebalance without bridge (recovered intent)',
|
|
306
|
+
);
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
const bridgeConfig = this.getBridgeConfigForRoute(
|
|
310
|
+
rebalance.origin,
|
|
311
|
+
rebalance.destination,
|
|
312
|
+
);
|
|
313
|
+
return bridgeConfig?.bridge === rebalance.bridge;
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Build a map from deficit chains to their transfer origin chains.
|
|
319
|
+
* This identifies which surplus chains are "natural" sources for each deficit.
|
|
320
|
+
*/
|
|
321
|
+
private buildTransferOriginMap(
|
|
322
|
+
pendingTransfers: Route[],
|
|
323
|
+
deficitChains: Set<ChainName>,
|
|
324
|
+
): Map<ChainName, Set<ChainName>> {
|
|
325
|
+
const originMap = new Map<ChainName, Set<ChainName>>();
|
|
326
|
+
|
|
327
|
+
for (const transfer of pendingTransfers) {
|
|
328
|
+
// Only track transfers TO deficit chains
|
|
329
|
+
if (deficitChains.has(transfer.destination)) {
|
|
330
|
+
if (!originMap.has(transfer.destination)) {
|
|
331
|
+
originMap.set(transfer.destination, new Set());
|
|
332
|
+
}
|
|
333
|
+
originMap.get(transfer.destination)!.add(transfer.origin);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return originMap;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Sort surpluses to prefer transfer origins over largest balances.
|
|
342
|
+
*
|
|
343
|
+
* Sorting priority:
|
|
344
|
+
* 1. Chains that are origins of transfers TO any deficit chain (preferred)
|
|
345
|
+
* 2. By amount descending (tiebreaker)
|
|
346
|
+
*/
|
|
347
|
+
private sortSurplusesWithOriginPreference(
|
|
348
|
+
surpluses: Delta[],
|
|
349
|
+
transferOriginMap: Map<ChainName, Set<ChainName>>,
|
|
350
|
+
): void {
|
|
351
|
+
// Collect all origin chains across all deficits
|
|
352
|
+
const allOriginChains = new Set<ChainName>();
|
|
353
|
+
for (const origins of transferOriginMap.values()) {
|
|
354
|
+
for (const origin of origins) {
|
|
355
|
+
allOriginChains.add(origin);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
surpluses.sort((a, b) => {
|
|
360
|
+
const aIsOrigin = allOriginChains.has(a.chain);
|
|
361
|
+
const bIsOrigin = allOriginChains.has(b.chain);
|
|
362
|
+
|
|
363
|
+
// Prefer transfer origins
|
|
364
|
+
if (aIsOrigin && !bIsOrigin) return -1;
|
|
365
|
+
if (!aIsOrigin && bIsOrigin) return 1;
|
|
366
|
+
|
|
367
|
+
// Tiebreaker: larger amount first
|
|
368
|
+
return a.amount > b.amount ? -1 : 1;
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
if (allOriginChains.size > 0) {
|
|
372
|
+
this.logger.debug(
|
|
373
|
+
{
|
|
374
|
+
context: this.constructor.name,
|
|
375
|
+
preferredOrigins: Array.from(allOriginChains),
|
|
376
|
+
sortedSurpluses: surpluses.map((s) => s.chain),
|
|
377
|
+
},
|
|
378
|
+
'Sorted surpluses with transfer origin preference',
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
protected getTokenByChainName(chainName: string): Token {
|
|
384
|
+
const token = this.tokensByChainName![chainName];
|
|
385
|
+
if (token === undefined) {
|
|
386
|
+
throw new Error(`Token not found for chain ${chainName}`);
|
|
387
|
+
}
|
|
388
|
+
return token;
|
|
389
|
+
}
|
|
390
|
+
}
|