@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,647 @@
|
|
|
1
|
+
import type { Logger } from 'pino';
|
|
2
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
3
|
+
|
|
4
|
+
import type { HyperlaneCore } from '@hyperlane-xyz/sdk';
|
|
5
|
+
import type { Address, Domain } from '@hyperlane-xyz/utils';
|
|
6
|
+
import { parseWarpRouteMessage } from '@hyperlane-xyz/utils';
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
ConfirmedBlockTag,
|
|
10
|
+
ConfirmedBlockTags,
|
|
11
|
+
} from '../interfaces/IMonitor.js';
|
|
12
|
+
import type {
|
|
13
|
+
ExplorerClient,
|
|
14
|
+
ExplorerMessage,
|
|
15
|
+
} from '../utils/ExplorerClient.js';
|
|
16
|
+
|
|
17
|
+
import type {
|
|
18
|
+
CreateRebalanceActionParams,
|
|
19
|
+
CreateRebalanceIntentParams,
|
|
20
|
+
IActionTracker,
|
|
21
|
+
} from './IActionTracker.js';
|
|
22
|
+
import type {
|
|
23
|
+
IRebalanceActionStore,
|
|
24
|
+
IRebalanceIntentStore,
|
|
25
|
+
ITransferStore,
|
|
26
|
+
RebalanceAction,
|
|
27
|
+
RebalanceIntent,
|
|
28
|
+
Transfer,
|
|
29
|
+
} from './types.js';
|
|
30
|
+
|
|
31
|
+
export interface ActionTrackerConfig {
|
|
32
|
+
routersByDomain: Record<number, string>; // Domain ID → router address (source of truth for routers and domains)
|
|
33
|
+
bridges: Address[]; // Bridge contract addresses for rebalance action queries
|
|
34
|
+
rebalancerAddress: Address;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* ActionTracker implementation managing the lifecycle of tracked entities.
|
|
39
|
+
*/
|
|
40
|
+
export class ActionTracker implements IActionTracker {
|
|
41
|
+
constructor(
|
|
42
|
+
private readonly transferStore: ITransferStore,
|
|
43
|
+
private readonly rebalanceIntentStore: IRebalanceIntentStore,
|
|
44
|
+
private readonly rebalanceActionStore: IRebalanceActionStore,
|
|
45
|
+
private readonly explorerClient: ExplorerClient,
|
|
46
|
+
private readonly core: HyperlaneCore,
|
|
47
|
+
private readonly config: ActionTrackerConfig,
|
|
48
|
+
private readonly logger: Logger,
|
|
49
|
+
) {}
|
|
50
|
+
|
|
51
|
+
// === Lifecycle ===
|
|
52
|
+
|
|
53
|
+
async initialize(): Promise<void> {
|
|
54
|
+
this.logger.info('ActionTracker initializing');
|
|
55
|
+
|
|
56
|
+
// Log config for debugging
|
|
57
|
+
this.logger.debug(
|
|
58
|
+
{
|
|
59
|
+
routersByDomain: this.config.routersByDomain,
|
|
60
|
+
bridges: this.config.bridges,
|
|
61
|
+
rebalancerAddress: this.config.rebalancerAddress,
|
|
62
|
+
},
|
|
63
|
+
'ActionTracker config',
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
// 1. Startup recovery: query Explorer for inflight rebalance messages
|
|
67
|
+
const inflightMessages =
|
|
68
|
+
await this.explorerClient.getInflightRebalanceActions(
|
|
69
|
+
{
|
|
70
|
+
bridges: this.config.bridges,
|
|
71
|
+
routersByDomain: this.config.routersByDomain,
|
|
72
|
+
rebalancerAddress: this.config.rebalancerAddress,
|
|
73
|
+
},
|
|
74
|
+
this.logger,
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
this.logger.info(
|
|
78
|
+
{ count: inflightMessages.length },
|
|
79
|
+
'Found inflight rebalance messages during startup',
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
// 2. For each message, create synthetic intent + action
|
|
83
|
+
for (const msg of inflightMessages) {
|
|
84
|
+
await this.recoverAction(msg);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 3. Sync all stores
|
|
88
|
+
await this.syncTransfers();
|
|
89
|
+
await this.syncRebalanceIntents();
|
|
90
|
+
await this.syncRebalanceActions();
|
|
91
|
+
|
|
92
|
+
// Log store contents for debugging
|
|
93
|
+
await this.logStoreContents();
|
|
94
|
+
|
|
95
|
+
this.logger.info('ActionTracker initialized');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// === Sync Operations ===
|
|
99
|
+
|
|
100
|
+
async syncTransfers(confirmedBlockTags?: ConfirmedBlockTags): Promise<void> {
|
|
101
|
+
this.logger.debug('Syncing transfers');
|
|
102
|
+
|
|
103
|
+
const inflightMessages = await this.explorerClient.getInflightUserTransfers(
|
|
104
|
+
{
|
|
105
|
+
routersByDomain: this.config.routersByDomain,
|
|
106
|
+
excludeTxSender: this.config.rebalancerAddress,
|
|
107
|
+
},
|
|
108
|
+
this.logger,
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
this.logger.debug(
|
|
112
|
+
{ count: inflightMessages.length },
|
|
113
|
+
'Received inflight user transfers from Explorer',
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
let newTransfers = 0;
|
|
117
|
+
let completedTransfers = 0;
|
|
118
|
+
|
|
119
|
+
for (const msg of inflightMessages) {
|
|
120
|
+
const transfer = await this.transferStore.get(msg.msg_id);
|
|
121
|
+
|
|
122
|
+
if (!transfer) {
|
|
123
|
+
this.logger.debug(
|
|
124
|
+
{
|
|
125
|
+
msgId: msg.msg_id,
|
|
126
|
+
origin: msg.origin_domain_id,
|
|
127
|
+
destination: msg.destination_domain_id,
|
|
128
|
+
sender: msg.sender,
|
|
129
|
+
recipient: msg.recipient,
|
|
130
|
+
messageBodyLength: msg.message_body?.length,
|
|
131
|
+
messageBodyPreview: msg.message_body?.substring(0, 66),
|
|
132
|
+
},
|
|
133
|
+
'Processing new transfer message',
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const { amount } = parseWarpRouteMessage(msg.message_body);
|
|
138
|
+
const newTransfer: Transfer = {
|
|
139
|
+
id: msg.msg_id,
|
|
140
|
+
status: 'in_progress',
|
|
141
|
+
messageId: msg.msg_id,
|
|
142
|
+
origin: msg.origin_domain_id,
|
|
143
|
+
destination: msg.destination_domain_id,
|
|
144
|
+
sender: msg.sender,
|
|
145
|
+
recipient: msg.recipient,
|
|
146
|
+
amount,
|
|
147
|
+
createdAt: Date.now(),
|
|
148
|
+
updatedAt: Date.now(),
|
|
149
|
+
};
|
|
150
|
+
await this.transferStore.save(newTransfer);
|
|
151
|
+
newTransfers++;
|
|
152
|
+
this.logger.debug(
|
|
153
|
+
{ id: newTransfer.id, amount: amount.toString() },
|
|
154
|
+
'Created new transfer',
|
|
155
|
+
);
|
|
156
|
+
} catch (error) {
|
|
157
|
+
this.logger.warn(
|
|
158
|
+
{
|
|
159
|
+
msgId: msg.msg_id,
|
|
160
|
+
messageBody: msg.message_body,
|
|
161
|
+
messageBodyLength: msg.message_body?.length,
|
|
162
|
+
origin: msg.origin_domain_id,
|
|
163
|
+
destination: msg.destination_domain_id,
|
|
164
|
+
error: error instanceof Error ? error.message : String(error),
|
|
165
|
+
},
|
|
166
|
+
'Failed to parse message body, skipping transfer',
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const existingTransfers = await this.getInProgressTransfers();
|
|
173
|
+
for (const transfer of existingTransfers) {
|
|
174
|
+
const chainName = this.core.multiProvider.getChainName(
|
|
175
|
+
transfer.destination,
|
|
176
|
+
);
|
|
177
|
+
const blockTag = confirmedBlockTags?.[chainName];
|
|
178
|
+
|
|
179
|
+
const delivered = await this.isMessageDelivered(
|
|
180
|
+
transfer.messageId,
|
|
181
|
+
transfer.destination,
|
|
182
|
+
blockTag,
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
if (delivered) {
|
|
186
|
+
await this.transferStore.update(transfer.id, { status: 'complete' });
|
|
187
|
+
completedTransfers++;
|
|
188
|
+
this.logger.debug({ id: transfer.id }, 'Transfer completed');
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const inProgressCount = (await this.getInProgressTransfers()).length;
|
|
193
|
+
this.logger.info(
|
|
194
|
+
{
|
|
195
|
+
newTransfers,
|
|
196
|
+
completed: completedTransfers,
|
|
197
|
+
inProgress: inProgressCount,
|
|
198
|
+
},
|
|
199
|
+
'Transfers synced',
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async syncRebalanceIntents(): Promise<void> {
|
|
204
|
+
this.logger.debug('Syncing rebalance intents');
|
|
205
|
+
|
|
206
|
+
// Check in_progress intents for completion
|
|
207
|
+
const inProgressIntents =
|
|
208
|
+
await this.rebalanceIntentStore.getByStatus('in_progress');
|
|
209
|
+
for (const intent of inProgressIntents) {
|
|
210
|
+
if (intent.fulfilledAmount >= intent.amount) {
|
|
211
|
+
await this.rebalanceIntentStore.update(intent.id, {
|
|
212
|
+
status: 'complete',
|
|
213
|
+
});
|
|
214
|
+
this.logger.debug({ id: intent.id }, 'RebalanceIntent completed');
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
this.logger.debug('Rebalance intents synced');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async syncRebalanceActions(
|
|
222
|
+
confirmedBlockTags?: ConfirmedBlockTags,
|
|
223
|
+
): Promise<void> {
|
|
224
|
+
this.logger.debug('Syncing rebalance actions');
|
|
225
|
+
|
|
226
|
+
let discoveredActions = 0;
|
|
227
|
+
let completedActions = 0;
|
|
228
|
+
|
|
229
|
+
const inflightMessages =
|
|
230
|
+
await this.explorerClient.getInflightRebalanceActions(
|
|
231
|
+
{
|
|
232
|
+
bridges: this.config.bridges,
|
|
233
|
+
routersByDomain: this.config.routersByDomain,
|
|
234
|
+
rebalancerAddress: this.config.rebalancerAddress,
|
|
235
|
+
},
|
|
236
|
+
this.logger,
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
this.logger.debug(
|
|
240
|
+
{ count: inflightMessages.length },
|
|
241
|
+
'Found inflight rebalance actions from Explorer',
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
const allActions = await this.rebalanceActionStore.getAll();
|
|
245
|
+
|
|
246
|
+
for (const msg of inflightMessages) {
|
|
247
|
+
const existingAction = allActions.find((a) => a.messageId === msg.msg_id);
|
|
248
|
+
|
|
249
|
+
if (!existingAction) {
|
|
250
|
+
this.logger.info(
|
|
251
|
+
{
|
|
252
|
+
msgId: msg.msg_id,
|
|
253
|
+
origin: msg.origin_domain_id,
|
|
254
|
+
destination: msg.destination_domain_id,
|
|
255
|
+
},
|
|
256
|
+
'Discovered new rebalance action, recovering...',
|
|
257
|
+
);
|
|
258
|
+
await this.recoverAction(msg);
|
|
259
|
+
discoveredActions++;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const inProgressActions =
|
|
264
|
+
await this.rebalanceActionStore.getByStatus('in_progress');
|
|
265
|
+
for (const action of inProgressActions) {
|
|
266
|
+
const chainName = this.core.multiProvider.getChainName(
|
|
267
|
+
action.destination,
|
|
268
|
+
);
|
|
269
|
+
const blockTag = confirmedBlockTags?.[chainName];
|
|
270
|
+
|
|
271
|
+
const delivered = await this.isMessageDelivered(
|
|
272
|
+
action.messageId,
|
|
273
|
+
action.destination,
|
|
274
|
+
blockTag,
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
if (delivered) {
|
|
278
|
+
await this.completeRebalanceAction(action.id);
|
|
279
|
+
completedActions++;
|
|
280
|
+
this.logger.debug({ id: action.id }, 'RebalanceAction completed');
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const inProgressCount = (
|
|
285
|
+
await this.rebalanceActionStore.getByStatus('in_progress')
|
|
286
|
+
).length;
|
|
287
|
+
this.logger.info(
|
|
288
|
+
{
|
|
289
|
+
discovered: discoveredActions,
|
|
290
|
+
completed: completedActions,
|
|
291
|
+
inProgress: inProgressCount,
|
|
292
|
+
},
|
|
293
|
+
'Actions synced',
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// === Transfer Queries ===
|
|
298
|
+
|
|
299
|
+
async getInProgressTransfers(): Promise<Transfer[]> {
|
|
300
|
+
return this.transferStore.getByStatus('in_progress');
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async getTransfersByDestination(destination: Domain): Promise<Transfer[]> {
|
|
304
|
+
return this.transferStore.getByDestination(destination);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// === RebalanceIntent Queries ===
|
|
308
|
+
|
|
309
|
+
async getActiveRebalanceIntents(): Promise<RebalanceIntent[]> {
|
|
310
|
+
// Only return in_progress intents - their origin tx is confirmed
|
|
311
|
+
// so simulation only needs to add to destination (origin already deducted on-chain)
|
|
312
|
+
return this.rebalanceIntentStore.getByStatus('in_progress');
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async getRebalanceIntentsByDestination(
|
|
316
|
+
destination: Domain,
|
|
317
|
+
): Promise<RebalanceIntent[]> {
|
|
318
|
+
return this.rebalanceIntentStore.getByDestination(destination);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// === RebalanceIntent Management ===
|
|
322
|
+
|
|
323
|
+
async createRebalanceIntent(
|
|
324
|
+
params: CreateRebalanceIntentParams,
|
|
325
|
+
): Promise<RebalanceIntent> {
|
|
326
|
+
const intent: RebalanceIntent = {
|
|
327
|
+
id: uuidv4(),
|
|
328
|
+
status: 'not_started',
|
|
329
|
+
origin: params.origin,
|
|
330
|
+
destination: params.destination,
|
|
331
|
+
amount: params.amount,
|
|
332
|
+
fulfilledAmount: 0n,
|
|
333
|
+
bridge: params.bridge,
|
|
334
|
+
priority: params.priority,
|
|
335
|
+
strategyType: params.strategyType,
|
|
336
|
+
createdAt: Date.now(),
|
|
337
|
+
updatedAt: Date.now(),
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
await this.rebalanceIntentStore.save(intent);
|
|
341
|
+
this.logger.debug(
|
|
342
|
+
{ id: intent.id, origin: intent.origin, destination: intent.destination },
|
|
343
|
+
'Created RebalanceIntent',
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
return intent;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async completeRebalanceIntent(id: string): Promise<void> {
|
|
350
|
+
await this.rebalanceIntentStore.update(id, { status: 'complete' });
|
|
351
|
+
this.logger.info({ id }, 'Intent completed');
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async cancelRebalanceIntent(id: string): Promise<void> {
|
|
355
|
+
await this.rebalanceIntentStore.update(id, { status: 'cancelled' });
|
|
356
|
+
this.logger.debug({ id }, 'Cancelled RebalanceIntent');
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async failRebalanceIntent(id: string): Promise<void> {
|
|
360
|
+
await this.rebalanceIntentStore.update(id, { status: 'failed' });
|
|
361
|
+
this.logger.info({ id }, 'Intent failed');
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// === RebalanceAction Management ===
|
|
365
|
+
|
|
366
|
+
async createRebalanceAction(
|
|
367
|
+
params: CreateRebalanceActionParams,
|
|
368
|
+
): Promise<RebalanceAction> {
|
|
369
|
+
const action: RebalanceAction = {
|
|
370
|
+
id: uuidv4(),
|
|
371
|
+
status: 'in_progress',
|
|
372
|
+
intentId: params.intentId,
|
|
373
|
+
messageId: params.messageId,
|
|
374
|
+
txHash: params.txHash,
|
|
375
|
+
origin: params.origin,
|
|
376
|
+
destination: params.destination,
|
|
377
|
+
amount: params.amount,
|
|
378
|
+
createdAt: Date.now(),
|
|
379
|
+
updatedAt: Date.now(),
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
await this.rebalanceActionStore.save(action);
|
|
383
|
+
|
|
384
|
+
// Transition parent intent from not_started to in_progress
|
|
385
|
+
const intent = await this.rebalanceIntentStore.get(params.intentId);
|
|
386
|
+
if (intent && intent.status === 'not_started') {
|
|
387
|
+
await this.rebalanceIntentStore.update(intent.id, {
|
|
388
|
+
status: 'in_progress',
|
|
389
|
+
});
|
|
390
|
+
this.logger.debug(
|
|
391
|
+
{ intentId: intent.id },
|
|
392
|
+
'Transitioned RebalanceIntent to in_progress',
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
this.logger.debug(
|
|
397
|
+
{ id: action.id, intentId: action.intentId },
|
|
398
|
+
'Created RebalanceAction',
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
return action;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
async completeRebalanceAction(id: string): Promise<void> {
|
|
405
|
+
const action = await this.rebalanceActionStore.get(id);
|
|
406
|
+
if (!action) {
|
|
407
|
+
throw new Error(`RebalanceAction ${id} not found`);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
await this.rebalanceActionStore.update(id, { status: 'complete' });
|
|
411
|
+
|
|
412
|
+
// Update parent intent's fulfilledAmount
|
|
413
|
+
const intent = await this.rebalanceIntentStore.get(action.intentId);
|
|
414
|
+
if (intent) {
|
|
415
|
+
const newFulfilledAmount = intent.fulfilledAmount + action.amount;
|
|
416
|
+
const updates: Partial<RebalanceIntent> = {
|
|
417
|
+
fulfilledAmount: newFulfilledAmount,
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
// Check if intent is now complete
|
|
421
|
+
if (newFulfilledAmount >= intent.amount) {
|
|
422
|
+
updates.status = 'complete';
|
|
423
|
+
this.logger.debug(
|
|
424
|
+
{ intentId: intent.id },
|
|
425
|
+
'RebalanceIntent fully fulfilled',
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
await this.rebalanceIntentStore.update(intent.id, updates);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
this.logger.info({ id, intentId: action.intentId }, 'Action completed');
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
async failRebalanceAction(id: string): Promise<void> {
|
|
436
|
+
await this.rebalanceActionStore.update(id, { status: 'failed' });
|
|
437
|
+
this.logger.info({ id }, 'Action failed');
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// === Debug Helpers ===
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Log the contents of all stores.
|
|
444
|
+
* Logs each item separately for full visibility (avoids [Object] truncation).
|
|
445
|
+
*/
|
|
446
|
+
async logStoreContents(): Promise<void> {
|
|
447
|
+
const transfers = await this.transferStore.getAll();
|
|
448
|
+
const intents = await this.rebalanceIntentStore.getAll();
|
|
449
|
+
const actions = await this.rebalanceActionStore.getAll();
|
|
450
|
+
|
|
451
|
+
const activeIntents = intents.filter((i) =>
|
|
452
|
+
['not_started', 'in_progress'].includes(i.status),
|
|
453
|
+
);
|
|
454
|
+
const inProgressTransfers = transfers.filter(
|
|
455
|
+
(t) => t.status === 'in_progress',
|
|
456
|
+
);
|
|
457
|
+
const inProgressActions = actions.filter((a) => a.status === 'in_progress');
|
|
458
|
+
|
|
459
|
+
// Log summary
|
|
460
|
+
this.logger.info(
|
|
461
|
+
{
|
|
462
|
+
transfers: inProgressTransfers.length,
|
|
463
|
+
intents: activeIntents.length,
|
|
464
|
+
actions: inProgressActions.length,
|
|
465
|
+
},
|
|
466
|
+
'Store summary',
|
|
467
|
+
);
|
|
468
|
+
|
|
469
|
+
// Log each transfer separately
|
|
470
|
+
for (const t of inProgressTransfers) {
|
|
471
|
+
this.logger.info(
|
|
472
|
+
{
|
|
473
|
+
type: 'transfer',
|
|
474
|
+
origin: t.origin,
|
|
475
|
+
destination: t.destination,
|
|
476
|
+
amount: t.amount.toString(),
|
|
477
|
+
messageId: t.messageId,
|
|
478
|
+
},
|
|
479
|
+
'In-progress transfer',
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Log each intent separately
|
|
484
|
+
for (const i of activeIntents) {
|
|
485
|
+
this.logger.info(
|
|
486
|
+
{
|
|
487
|
+
type: 'intent',
|
|
488
|
+
id: i.id,
|
|
489
|
+
origin: i.origin,
|
|
490
|
+
destination: i.destination,
|
|
491
|
+
amount: i.amount.toString(),
|
|
492
|
+
status: i.status,
|
|
493
|
+
bridge: i.bridge,
|
|
494
|
+
},
|
|
495
|
+
'Active intent',
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Log each action separately
|
|
500
|
+
for (const a of inProgressActions) {
|
|
501
|
+
this.logger.info(
|
|
502
|
+
{
|
|
503
|
+
type: 'action',
|
|
504
|
+
id: a.id,
|
|
505
|
+
origin: a.origin,
|
|
506
|
+
destination: a.destination,
|
|
507
|
+
amount: a.amount.toString(),
|
|
508
|
+
messageId: a.messageId,
|
|
509
|
+
intentId: a.intentId,
|
|
510
|
+
},
|
|
511
|
+
'In-progress action',
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// === Private Helpers ===
|
|
517
|
+
|
|
518
|
+
private async getConfirmedBlockTag(
|
|
519
|
+
chainName: string,
|
|
520
|
+
): Promise<ConfirmedBlockTag> {
|
|
521
|
+
try {
|
|
522
|
+
const metadata = this.core.multiProvider.getChainMetadata(chainName);
|
|
523
|
+
const reorgPeriod = metadata.blocks?.reorgPeriod ?? 32;
|
|
524
|
+
|
|
525
|
+
if (typeof reorgPeriod === 'string') {
|
|
526
|
+
return reorgPeriod as ConfirmedBlockTag;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const provider = this.core.multiProvider.getProvider(chainName);
|
|
530
|
+
const latestBlock = await provider.getBlockNumber();
|
|
531
|
+
return Math.max(0, latestBlock - reorgPeriod);
|
|
532
|
+
} catch (error) {
|
|
533
|
+
this.logger.warn(
|
|
534
|
+
{ chain: chainName, error: (error as Error).message },
|
|
535
|
+
'Failed to get confirmed block, using latest',
|
|
536
|
+
);
|
|
537
|
+
return undefined;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
private async isMessageDelivered(
|
|
542
|
+
messageId: string,
|
|
543
|
+
destination: Domain,
|
|
544
|
+
providedBlockTag?: ConfirmedBlockTag,
|
|
545
|
+
): Promise<boolean> {
|
|
546
|
+
try {
|
|
547
|
+
const chainName = this.core.multiProvider.getChainName(destination);
|
|
548
|
+
const mailbox = this.core.getContracts(chainName).mailbox;
|
|
549
|
+
|
|
550
|
+
const blockTag =
|
|
551
|
+
providedBlockTag ?? (await this.getConfirmedBlockTag(chainName));
|
|
552
|
+
const delivered = await mailbox.delivered(messageId, { blockTag });
|
|
553
|
+
|
|
554
|
+
this.logger.debug(
|
|
555
|
+
{ messageId, destination: chainName, blockTag, delivered },
|
|
556
|
+
'Checked message delivery at confirmed block',
|
|
557
|
+
);
|
|
558
|
+
|
|
559
|
+
return delivered;
|
|
560
|
+
} catch (error) {
|
|
561
|
+
this.logger.warn(
|
|
562
|
+
{ messageId, destination, error },
|
|
563
|
+
'Failed to check message delivery status',
|
|
564
|
+
);
|
|
565
|
+
return false;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
private async recoverAction(msg: ExplorerMessage): Promise<void> {
|
|
570
|
+
// Check if action already exists
|
|
571
|
+
const existing = await this.rebalanceActionStore.get(msg.msg_id);
|
|
572
|
+
if (existing) {
|
|
573
|
+
this.logger.debug({ id: msg.msg_id }, 'Action already exists, skipping');
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
this.logger.debug(
|
|
578
|
+
{
|
|
579
|
+
msgId: msg.msg_id,
|
|
580
|
+
origin: msg.origin_domain_id,
|
|
581
|
+
destination: msg.destination_domain_id,
|
|
582
|
+
sender: msg.sender,
|
|
583
|
+
recipient: msg.recipient,
|
|
584
|
+
txHash: msg.origin_tx_hash,
|
|
585
|
+
messageBodyLength: msg.message_body?.length,
|
|
586
|
+
messageBodyPreview: msg.message_body?.substring(0, 66),
|
|
587
|
+
},
|
|
588
|
+
'Recovering rebalance action',
|
|
589
|
+
);
|
|
590
|
+
|
|
591
|
+
try {
|
|
592
|
+
// Create synthetic intent
|
|
593
|
+
const { amount } = parseWarpRouteMessage(msg.message_body);
|
|
594
|
+
const intent: RebalanceIntent = {
|
|
595
|
+
id: uuidv4(),
|
|
596
|
+
status: 'in_progress',
|
|
597
|
+
origin: msg.origin_domain_id,
|
|
598
|
+
destination: msg.destination_domain_id,
|
|
599
|
+
amount,
|
|
600
|
+
fulfilledAmount: 0n,
|
|
601
|
+
priority: undefined,
|
|
602
|
+
strategyType: undefined,
|
|
603
|
+
createdAt: Date.now(),
|
|
604
|
+
updatedAt: Date.now(),
|
|
605
|
+
};
|
|
606
|
+
|
|
607
|
+
await this.rebalanceIntentStore.save(intent);
|
|
608
|
+
this.logger.debug(
|
|
609
|
+
{ id: intent.id, amount: amount.toString() },
|
|
610
|
+
'Created synthetic RebalanceIntent',
|
|
611
|
+
);
|
|
612
|
+
|
|
613
|
+
// Create action
|
|
614
|
+
const action: RebalanceAction = {
|
|
615
|
+
id: msg.msg_id,
|
|
616
|
+
status: 'in_progress',
|
|
617
|
+
intentId: intent.id,
|
|
618
|
+
messageId: msg.msg_id,
|
|
619
|
+
txHash: msg.origin_tx_hash,
|
|
620
|
+
origin: msg.origin_domain_id,
|
|
621
|
+
destination: msg.destination_domain_id,
|
|
622
|
+
amount,
|
|
623
|
+
createdAt: Date.now(),
|
|
624
|
+
updatedAt: Date.now(),
|
|
625
|
+
};
|
|
626
|
+
|
|
627
|
+
await this.rebalanceActionStore.save(action);
|
|
628
|
+
this.logger.debug(
|
|
629
|
+
{ id: action.id, intentId: action.intentId, amount: amount.toString() },
|
|
630
|
+
'Recovered RebalanceAction',
|
|
631
|
+
);
|
|
632
|
+
} catch (error) {
|
|
633
|
+
this.logger.warn(
|
|
634
|
+
{
|
|
635
|
+
msgId: msg.msg_id,
|
|
636
|
+
messageBody: msg.message_body,
|
|
637
|
+
messageBodyLength: msg.message_body?.length,
|
|
638
|
+
origin: msg.origin_domain_id,
|
|
639
|
+
destination: msg.destination_domain_id,
|
|
640
|
+
txHash: msg.origin_tx_hash,
|
|
641
|
+
error: error instanceof Error ? error.message : String(error),
|
|
642
|
+
},
|
|
643
|
+
'Failed to parse message body during recovery, skipping action',
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|