@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,558 @@
|
|
|
1
|
+
import { Contract, PopulatedTransaction } from 'ethers';
|
|
2
|
+
import { Logger } from 'pino';
|
|
3
|
+
|
|
4
|
+
import { IXERC20VS__factory } from '@hyperlane-xyz/core';
|
|
5
|
+
import {
|
|
6
|
+
EvmHypXERC20Adapter,
|
|
7
|
+
EvmHypXERC20LockboxAdapter,
|
|
8
|
+
EvmTokenAdapter,
|
|
9
|
+
IHypXERC20Adapter,
|
|
10
|
+
SealevelHypTokenAdapter,
|
|
11
|
+
Token,
|
|
12
|
+
TokenAmount,
|
|
13
|
+
TokenStandard,
|
|
14
|
+
TokenType,
|
|
15
|
+
WarpCore,
|
|
16
|
+
WarpRouteDeployConfig,
|
|
17
|
+
} from '@hyperlane-xyz/sdk';
|
|
18
|
+
import { Address, ProtocolType } from '@hyperlane-xyz/utils';
|
|
19
|
+
|
|
20
|
+
import { IMetrics } from '../interfaces/IMetrics.js';
|
|
21
|
+
import { MonitorEvent } from '../interfaces/IMonitor.js';
|
|
22
|
+
import { RebalancingRoute } from '../interfaces/IStrategy.js';
|
|
23
|
+
import { formatBigInt, tryFn } from '../utils/index.js';
|
|
24
|
+
|
|
25
|
+
import { PriceGetter } from './PriceGetter.js';
|
|
26
|
+
import {
|
|
27
|
+
metricsRegister,
|
|
28
|
+
rebalancerExecutionAmount,
|
|
29
|
+
rebalancerExecutionTotal,
|
|
30
|
+
rebalancerPollingErrorsTotal,
|
|
31
|
+
updateManagedLockboxBalanceMetrics,
|
|
32
|
+
updateNativeWalletBalanceMetrics,
|
|
33
|
+
updateTokenBalanceMetrics,
|
|
34
|
+
updateXERC20LimitsMetrics,
|
|
35
|
+
} from './scripts/metrics.js';
|
|
36
|
+
import {
|
|
37
|
+
NativeWalletBalance,
|
|
38
|
+
WarpRouteBalance,
|
|
39
|
+
XERC20Info,
|
|
40
|
+
XERC20Limit,
|
|
41
|
+
} from './types.js';
|
|
42
|
+
import { startMetricsServer } from './utils/metrics.js';
|
|
43
|
+
|
|
44
|
+
export class Metrics implements IMetrics {
|
|
45
|
+
private readonly managedLockBoxMinimalABI = [
|
|
46
|
+
'function XERC20() view returns (address)',
|
|
47
|
+
'function ERC20() view returns (address)',
|
|
48
|
+
] as const;
|
|
49
|
+
private readonly logger: Logger;
|
|
50
|
+
|
|
51
|
+
constructor(
|
|
52
|
+
private readonly tokenPriceGetter: PriceGetter,
|
|
53
|
+
private readonly warpDeployConfig: WarpRouteDeployConfig | null,
|
|
54
|
+
private readonly warpCore: WarpCore,
|
|
55
|
+
private readonly warpRouteId: string,
|
|
56
|
+
logger: Logger,
|
|
57
|
+
) {
|
|
58
|
+
this.logger = logger.child({ class: Metrics.name });
|
|
59
|
+
startMetricsServer(metricsRegister);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
recordRebalancerSuccess() {
|
|
63
|
+
rebalancerExecutionTotal
|
|
64
|
+
.labels({ warp_route_id: this.warpRouteId, succeeded: 'true' })
|
|
65
|
+
.inc();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
recordRebalanceAmount(
|
|
69
|
+
route: RebalancingRoute,
|
|
70
|
+
originTokenAmount: TokenAmount,
|
|
71
|
+
) {
|
|
72
|
+
rebalancerExecutionAmount
|
|
73
|
+
.labels({
|
|
74
|
+
warp_route_id: this.warpRouteId,
|
|
75
|
+
origin: route.origin,
|
|
76
|
+
destination: route.destination,
|
|
77
|
+
token: originTokenAmount.token.symbol,
|
|
78
|
+
})
|
|
79
|
+
.inc(originTokenAmount.getDecimalFormattedAmount());
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
recordRebalancerFailure() {
|
|
83
|
+
rebalancerExecutionTotal
|
|
84
|
+
.labels({ warp_route_id: this.warpRouteId, succeeded: 'false' })
|
|
85
|
+
.inc();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
recordPollingError() {
|
|
89
|
+
rebalancerPollingErrorsTotal
|
|
90
|
+
.labels({ warp_route_id: this.warpRouteId })
|
|
91
|
+
.inc();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async processToken({
|
|
95
|
+
token,
|
|
96
|
+
bridgedSupply,
|
|
97
|
+
}: MonitorEvent['tokensInfo'][number]) {
|
|
98
|
+
await tryFn(
|
|
99
|
+
async () => {
|
|
100
|
+
await this.updateTokenMetrics(token, bridgedSupply);
|
|
101
|
+
},
|
|
102
|
+
'Updating warp route metrics',
|
|
103
|
+
this.logger,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Updates the metrics for a single token in a warp route.
|
|
108
|
+
private async updateTokenMetrics(
|
|
109
|
+
token: Token,
|
|
110
|
+
bridgedSupply?: bigint,
|
|
111
|
+
): Promise<void> {
|
|
112
|
+
const promises = [
|
|
113
|
+
tryFn(
|
|
114
|
+
async () => {
|
|
115
|
+
const balanceInfo = await this.getTokenBridgedBalance(
|
|
116
|
+
token,
|
|
117
|
+
bridgedSupply,
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
if (!balanceInfo) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
updateTokenBalanceMetrics(
|
|
125
|
+
this.warpCore,
|
|
126
|
+
token,
|
|
127
|
+
balanceInfo,
|
|
128
|
+
this.warpRouteId,
|
|
129
|
+
this.logger,
|
|
130
|
+
);
|
|
131
|
+
},
|
|
132
|
+
'Getting bridged balance and value',
|
|
133
|
+
this.logger,
|
|
134
|
+
),
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
// For Sealevel collateral and synthetic tokens, there is an
|
|
138
|
+
// "Associated Token Account" (ATA) rent payer that has a balance
|
|
139
|
+
// that's used to pay for rent for the accounts that store user balances.
|
|
140
|
+
// This is necessary if the recipient has never received any tokens before.
|
|
141
|
+
if (token.protocol === ProtocolType.Sealevel && !token.isNative()) {
|
|
142
|
+
promises.push(
|
|
143
|
+
tryFn(
|
|
144
|
+
async () => {
|
|
145
|
+
const balance = await this.getSealevelAtaPayerBalance(
|
|
146
|
+
token,
|
|
147
|
+
this.warpRouteId,
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
updateNativeWalletBalanceMetrics(balance, this.logger);
|
|
151
|
+
},
|
|
152
|
+
'Getting ATA payer balance',
|
|
153
|
+
this.logger,
|
|
154
|
+
),
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (token.isXerc20()) {
|
|
159
|
+
promises.push(
|
|
160
|
+
tryFn(
|
|
161
|
+
async () => {
|
|
162
|
+
const { limits, xERC20Address } = await this.getXERC20Info(token);
|
|
163
|
+
const routerAddress = token.addressOrDenom;
|
|
164
|
+
|
|
165
|
+
updateXERC20LimitsMetrics(
|
|
166
|
+
token,
|
|
167
|
+
limits,
|
|
168
|
+
routerAddress,
|
|
169
|
+
token.standard,
|
|
170
|
+
xERC20Address,
|
|
171
|
+
this.logger,
|
|
172
|
+
);
|
|
173
|
+
},
|
|
174
|
+
'Getting xERC20 limits',
|
|
175
|
+
this.logger,
|
|
176
|
+
),
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
if (!this.warpDeployConfig) {
|
|
180
|
+
this.logger.warn(
|
|
181
|
+
{
|
|
182
|
+
tokenSymbol: token.symbol,
|
|
183
|
+
chain: token.chainName,
|
|
184
|
+
},
|
|
185
|
+
"Can't read warp deploy config, skipping extra lockboxes",
|
|
186
|
+
);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// If the current token is an xERC20, we need to check if there are any extra lockboxes
|
|
191
|
+
const currentTokenDeployConfig = this.warpDeployConfig[token.chainName];
|
|
192
|
+
|
|
193
|
+
if (
|
|
194
|
+
currentTokenDeployConfig.type !== TokenType.XERC20 &&
|
|
195
|
+
currentTokenDeployConfig.type !== TokenType.XERC20Lockbox
|
|
196
|
+
) {
|
|
197
|
+
this.logger.error(
|
|
198
|
+
{
|
|
199
|
+
tokenSymbol: token.symbol,
|
|
200
|
+
chain: token.chainName,
|
|
201
|
+
expectedType: [TokenType.XERC20, TokenType.XERC20Lockbox],
|
|
202
|
+
actualType: currentTokenDeployConfig.type,
|
|
203
|
+
},
|
|
204
|
+
'Token type mismatch in deploy config for xERC20 token',
|
|
205
|
+
);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const extraLockboxes =
|
|
210
|
+
currentTokenDeployConfig.xERC20?.extraBridges ?? [];
|
|
211
|
+
|
|
212
|
+
for (const lockbox of extraLockboxes) {
|
|
213
|
+
promises.push(
|
|
214
|
+
tryFn(
|
|
215
|
+
async () => {
|
|
216
|
+
const { limits, xERC20Address } = await this.getExtraLockboxInfo(
|
|
217
|
+
token,
|
|
218
|
+
lockbox.lockbox,
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
updateXERC20LimitsMetrics(
|
|
222
|
+
token,
|
|
223
|
+
limits,
|
|
224
|
+
lockbox.lockbox,
|
|
225
|
+
'EvmManagedLockbox',
|
|
226
|
+
xERC20Address,
|
|
227
|
+
this.logger,
|
|
228
|
+
);
|
|
229
|
+
},
|
|
230
|
+
'Getting extra lockbox limits',
|
|
231
|
+
this.logger,
|
|
232
|
+
),
|
|
233
|
+
tryFn(
|
|
234
|
+
async () => {
|
|
235
|
+
const balance = await this.getExtraLockboxBalance(
|
|
236
|
+
token,
|
|
237
|
+
lockbox.lockbox,
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
if (balance) {
|
|
241
|
+
const { tokenName, tokenAddress } =
|
|
242
|
+
await this.getManagedLockBoxCollateralInfo(
|
|
243
|
+
token,
|
|
244
|
+
lockbox.lockbox,
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
updateManagedLockboxBalanceMetrics(
|
|
248
|
+
this.warpCore,
|
|
249
|
+
token.chainName,
|
|
250
|
+
tokenName,
|
|
251
|
+
tokenAddress,
|
|
252
|
+
lockbox.lockbox,
|
|
253
|
+
balance,
|
|
254
|
+
this.warpRouteId,
|
|
255
|
+
this.logger,
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
`Updating extra lockbox balance for contract at "${lockbox.lockbox}" on chain ${token.chainName}`,
|
|
260
|
+
this.logger,
|
|
261
|
+
),
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
await Promise.all(promises);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Gets the bridged balance and value of a token in a warp route.
|
|
270
|
+
private async getTokenBridgedBalance(
|
|
271
|
+
token: Token,
|
|
272
|
+
bridgedSupply?: bigint,
|
|
273
|
+
): Promise<WarpRouteBalance | undefined> {
|
|
274
|
+
if (!token.isHypToken()) {
|
|
275
|
+
this.logger.warn(
|
|
276
|
+
{
|
|
277
|
+
tokenChain: token.chainName,
|
|
278
|
+
tokenSymbol: token.symbol,
|
|
279
|
+
tokenAddress: token.addressOrDenom,
|
|
280
|
+
},
|
|
281
|
+
'Cannot get bridged balance for a non-Hyperlane token',
|
|
282
|
+
);
|
|
283
|
+
return undefined;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const adapter = token.getHypAdapter(this.warpCore.multiProvider);
|
|
287
|
+
let tokenAddress = token.collateralAddressOrDenom ?? token.addressOrDenom;
|
|
288
|
+
|
|
289
|
+
if (bridgedSupply === undefined) {
|
|
290
|
+
this.logger.warn(
|
|
291
|
+
{
|
|
292
|
+
tokenChain: token.chainName,
|
|
293
|
+
tokenSymbol: token.symbol,
|
|
294
|
+
tokenAddress: token.addressOrDenom,
|
|
295
|
+
},
|
|
296
|
+
'Bridged supply not found for token',
|
|
297
|
+
);
|
|
298
|
+
return undefined;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const balance = token.amount(bridgedSupply).getDecimalFormattedAmount();
|
|
302
|
+
|
|
303
|
+
let tokenPrice;
|
|
304
|
+
// Only record value for collateralized and xERC20 lockbox tokens.
|
|
305
|
+
// removed the check for `EvmHypXERC20Lockbox` as it's already listed as collateralized
|
|
306
|
+
if (token.isCollateralized()) {
|
|
307
|
+
tokenPrice = await this.tokenPriceGetter.tryGetTokenPrice(token);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (token.standard === TokenStandard.EvmHypXERC20Lockbox) {
|
|
311
|
+
tokenAddress = (await (adapter as EvmHypXERC20LockboxAdapter).getXERC20())
|
|
312
|
+
.address;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
balance,
|
|
317
|
+
valueUSD: tokenPrice ? balance * tokenPrice : undefined,
|
|
318
|
+
tokenAddress,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Gets the native balance of the ATA payer, which is used to pay for
|
|
323
|
+
// rent when delivering tokens to an account that previously didn't
|
|
324
|
+
// have a balance.
|
|
325
|
+
// Only intended for Collateral or Synthetic Sealevel tokens.
|
|
326
|
+
private async getSealevelAtaPayerBalance(
|
|
327
|
+
token: Token,
|
|
328
|
+
warpRouteId: string,
|
|
329
|
+
): Promise<NativeWalletBalance> {
|
|
330
|
+
if (token.protocol !== ProtocolType.Sealevel || token.isNative()) {
|
|
331
|
+
throw new Error(
|
|
332
|
+
`Unsupported ATA payer protocol type ${token.protocol} or standard ${token.standard}`,
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
const adapter = token.getHypAdapter(
|
|
336
|
+
this.warpCore.multiProvider,
|
|
337
|
+
) as SealevelHypTokenAdapter;
|
|
338
|
+
|
|
339
|
+
const ataPayer = adapter.deriveAtaPayerAccount().toString();
|
|
340
|
+
const nativeToken = Token.FromChainMetadataNativeToken(
|
|
341
|
+
this.warpCore.multiProvider.getChainMetadata(token.chainName),
|
|
342
|
+
);
|
|
343
|
+
const ataPayerBalance = await nativeToken.getBalance(
|
|
344
|
+
this.warpCore.multiProvider,
|
|
345
|
+
ataPayer,
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
return {
|
|
349
|
+
chain: token.chainName,
|
|
350
|
+
walletAddress: ataPayer.toString(),
|
|
351
|
+
walletName: `${warpRouteId}/ata-payer`,
|
|
352
|
+
balance: ataPayerBalance.getDecimalFormattedAmount(),
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
private async getXERC20Info(token: Token): Promise<XERC20Info> {
|
|
357
|
+
if (token.protocol !== ProtocolType.Ethereum) {
|
|
358
|
+
throw new Error(`Unsupported XERC20 protocol type ${token.protocol}`);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (token.standard === TokenStandard.EvmHypXERC20) {
|
|
362
|
+
const adapter = token.getAdapter(
|
|
363
|
+
this.warpCore.multiProvider,
|
|
364
|
+
) as EvmHypXERC20Adapter;
|
|
365
|
+
return {
|
|
366
|
+
limits: await this.getXERC20Limit(token, adapter),
|
|
367
|
+
xERC20Address: (await adapter.getXERC20()).address,
|
|
368
|
+
};
|
|
369
|
+
} else if (token.standard === TokenStandard.EvmHypXERC20Lockbox) {
|
|
370
|
+
const adapter = token.getAdapter(
|
|
371
|
+
this.warpCore.multiProvider,
|
|
372
|
+
) as EvmHypXERC20LockboxAdapter;
|
|
373
|
+
return {
|
|
374
|
+
limits: await this.getXERC20Limit(token, adapter),
|
|
375
|
+
xERC20Address: (await adapter.getXERC20()).address,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
throw new Error(`Unsupported XERC20 token standard ${token.standard}`);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
private async getXERC20Limit(
|
|
382
|
+
token: Token,
|
|
383
|
+
xerc20: IHypXERC20Adapter<PopulatedTransaction>,
|
|
384
|
+
): Promise<XERC20Limit> {
|
|
385
|
+
const [mintCurrent, mintMax, burnCurrent, burnMax] = await Promise.all([
|
|
386
|
+
xerc20.getMintLimit(),
|
|
387
|
+
xerc20.getMintMaxLimit(),
|
|
388
|
+
xerc20.getBurnLimit(),
|
|
389
|
+
xerc20.getBurnMaxLimit(),
|
|
390
|
+
]);
|
|
391
|
+
|
|
392
|
+
return {
|
|
393
|
+
mint: formatBigInt(token, mintCurrent),
|
|
394
|
+
mintMax: formatBigInt(token, mintMax),
|
|
395
|
+
burn: formatBigInt(token, burnCurrent),
|
|
396
|
+
burnMax: formatBigInt(token, burnMax),
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
private async getExtraLockboxInfo(
|
|
401
|
+
warpToken: Token,
|
|
402
|
+
lockboxAddress: Address,
|
|
403
|
+
): Promise<XERC20Info> {
|
|
404
|
+
const currentChainProvider =
|
|
405
|
+
this.warpCore.multiProvider.getEthersV5Provider(warpToken.chainName);
|
|
406
|
+
const lockboxInstance = await this.getManagedLockBox(
|
|
407
|
+
warpToken,
|
|
408
|
+
lockboxAddress,
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
const xERC20Address = await lockboxInstance.XERC20();
|
|
412
|
+
const vsXERC20Address = IXERC20VS__factory.connect(
|
|
413
|
+
xERC20Address,
|
|
414
|
+
currentChainProvider,
|
|
415
|
+
); // todo use adapter
|
|
416
|
+
|
|
417
|
+
const [mintMax, burnMax, mint, burn] = await Promise.all([
|
|
418
|
+
vsXERC20Address.mintingMaxLimitOf(lockboxAddress),
|
|
419
|
+
vsXERC20Address.burningMaxLimitOf(lockboxAddress),
|
|
420
|
+
vsXERC20Address.mintingCurrentLimitOf(lockboxAddress),
|
|
421
|
+
vsXERC20Address.burningCurrentLimitOf(lockboxAddress),
|
|
422
|
+
]);
|
|
423
|
+
|
|
424
|
+
return {
|
|
425
|
+
limits: {
|
|
426
|
+
burn: formatBigInt(warpToken, burn.toBigInt()),
|
|
427
|
+
burnMax: formatBigInt(warpToken, burnMax.toBigInt()),
|
|
428
|
+
mint: formatBigInt(warpToken, mint.toBigInt()),
|
|
429
|
+
mintMax: formatBigInt(warpToken, mintMax.toBigInt()),
|
|
430
|
+
},
|
|
431
|
+
xERC20Address,
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
private async getManagedLockBox(
|
|
436
|
+
warpToken: Token,
|
|
437
|
+
lockboxAddress: Address,
|
|
438
|
+
): Promise<Contract> {
|
|
439
|
+
const chainName = warpToken.chainName;
|
|
440
|
+
const provider = this.warpCore.multiProvider.getEthersV5Provider(chainName);
|
|
441
|
+
|
|
442
|
+
return new Contract(
|
|
443
|
+
lockboxAddress,
|
|
444
|
+
this.managedLockBoxMinimalABI,
|
|
445
|
+
provider,
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
private async getExtraLockboxBalance(
|
|
450
|
+
warpToken: Token,
|
|
451
|
+
lockboxAddress: Address,
|
|
452
|
+
): Promise<WarpRouteBalance | undefined> {
|
|
453
|
+
if (!warpToken.isXerc20()) {
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const lockboxInstance = await this.getManagedLockBox(
|
|
458
|
+
warpToken,
|
|
459
|
+
lockboxAddress,
|
|
460
|
+
);
|
|
461
|
+
|
|
462
|
+
const erc20TokenAddress = await lockboxInstance.ERC20();
|
|
463
|
+
const erc20tokenAdapter = new EvmTokenAdapter(
|
|
464
|
+
warpToken.chainName,
|
|
465
|
+
this.warpCore.multiProvider,
|
|
466
|
+
{
|
|
467
|
+
token: erc20TokenAddress,
|
|
468
|
+
},
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
let balance;
|
|
472
|
+
try {
|
|
473
|
+
balance = await erc20tokenAdapter.getBalance(lockboxAddress);
|
|
474
|
+
} catch (error) {
|
|
475
|
+
this.logger.error(
|
|
476
|
+
{
|
|
477
|
+
lockboxAddress,
|
|
478
|
+
chain: warpToken.chainName,
|
|
479
|
+
erc20TokenAddress,
|
|
480
|
+
error: error as Error,
|
|
481
|
+
},
|
|
482
|
+
'Error getting balance for contract at lockbox',
|
|
483
|
+
);
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const tokenPrice = await this.tokenPriceGetter.tryGetTokenPrice(warpToken);
|
|
488
|
+
|
|
489
|
+
const balanceNumber = formatBigInt(warpToken, balance);
|
|
490
|
+
|
|
491
|
+
return {
|
|
492
|
+
balance: balanceNumber,
|
|
493
|
+
valueUSD: tokenPrice ? balanceNumber * tokenPrice : undefined,
|
|
494
|
+
tokenAddress: erc20TokenAddress,
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
private async getManagedLockBoxCollateralInfo(
|
|
499
|
+
warpToken: Token,
|
|
500
|
+
lockBoxAddress: Address,
|
|
501
|
+
): Promise<{ tokenName: string; tokenAddress: Address }> {
|
|
502
|
+
const lockBoxInstance = await this.getManagedLockBox(
|
|
503
|
+
warpToken,
|
|
504
|
+
lockBoxAddress,
|
|
505
|
+
);
|
|
506
|
+
|
|
507
|
+
const collateralTokenAddress = await lockBoxInstance.ERC20();
|
|
508
|
+
const collateralTokenAdapter = new EvmTokenAdapter(
|
|
509
|
+
warpToken.chainName,
|
|
510
|
+
this.warpCore.multiProvider,
|
|
511
|
+
{
|
|
512
|
+
token: collateralTokenAddress,
|
|
513
|
+
},
|
|
514
|
+
);
|
|
515
|
+
|
|
516
|
+
const { name } = await collateralTokenAdapter.getMetadata();
|
|
517
|
+
|
|
518
|
+
return {
|
|
519
|
+
tokenName: name,
|
|
520
|
+
tokenAddress: collateralTokenAddress,
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
static getWarpRouteCollateralTokenSymbol(tokens: Token[]): string {
|
|
525
|
+
// We need to have a deterministic way to determine the symbol of the warp route
|
|
526
|
+
// as its used to identify the warp route in metrics. This method should support routes where:
|
|
527
|
+
// - All tokens have the same symbol, token standards can be all collateral, all synthetic or a mix
|
|
528
|
+
// - All tokens have different symbol, but there is a collateral token to break the tie, where there are multiple collateral tokens, alphabetically first is chosen
|
|
529
|
+
// - All tokens have different symbol, but there is no collateral token to break the tie, pick the alphabetically first symbol
|
|
530
|
+
|
|
531
|
+
// Get all unique symbols from the tokens array
|
|
532
|
+
const uniqueSymbols = new Set(tokens.map((token) => token.symbol));
|
|
533
|
+
|
|
534
|
+
// If all tokens have the same symbol, return that symbol
|
|
535
|
+
if (uniqueSymbols.size === 1) {
|
|
536
|
+
return tokens[0].symbol;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Find all collateralized tokens
|
|
540
|
+
const collateralTokens = tokens.filter(
|
|
541
|
+
(token) =>
|
|
542
|
+
token.isCollateralized() ||
|
|
543
|
+
token.standard === TokenStandard.EvmHypXERC20Lockbox,
|
|
544
|
+
);
|
|
545
|
+
|
|
546
|
+
if (collateralTokens.length === 0) {
|
|
547
|
+
// If there are no collateralized tokens, return the alphabetically first symbol
|
|
548
|
+
return [...uniqueSymbols].sort()[0];
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// if there is a single unique collateral symbol return it or
|
|
552
|
+
// if there are multiple, return the alphabetically first symbol
|
|
553
|
+
const collateralSymbols = collateralTokens.map((token) => token.symbol);
|
|
554
|
+
const uniqueCollateralSymbols = [...new Set(collateralSymbols)];
|
|
555
|
+
|
|
556
|
+
return uniqueCollateralSymbols.sort()[0];
|
|
557
|
+
}
|
|
558
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Logger } from 'pino';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
ChainMap,
|
|
5
|
+
ChainMetadata,
|
|
6
|
+
CoinGeckoTokenPriceGetter,
|
|
7
|
+
Token,
|
|
8
|
+
} from '@hyperlane-xyz/sdk';
|
|
9
|
+
|
|
10
|
+
export class PriceGetter extends CoinGeckoTokenPriceGetter {
|
|
11
|
+
private readonly logger: Logger;
|
|
12
|
+
|
|
13
|
+
private constructor({
|
|
14
|
+
chainMetadata,
|
|
15
|
+
logger,
|
|
16
|
+
apiKey,
|
|
17
|
+
expirySeconds,
|
|
18
|
+
sleepMsBetweenRequests,
|
|
19
|
+
}: {
|
|
20
|
+
chainMetadata: ChainMap<ChainMetadata>;
|
|
21
|
+
logger: Logger;
|
|
22
|
+
apiKey?: string;
|
|
23
|
+
expirySeconds?: number;
|
|
24
|
+
sleepMsBetweenRequests?: number;
|
|
25
|
+
}) {
|
|
26
|
+
super({ chainMetadata, apiKey, expirySeconds, sleepMsBetweenRequests });
|
|
27
|
+
this.logger = logger;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public static create(
|
|
31
|
+
chainMetadata: ChainMap<ChainMetadata>,
|
|
32
|
+
logger: Logger,
|
|
33
|
+
coingeckoApiKey?: string,
|
|
34
|
+
expirySeconds?: number,
|
|
35
|
+
sleepMsBetweenRequests?: number,
|
|
36
|
+
) {
|
|
37
|
+
return new PriceGetter({
|
|
38
|
+
chainMetadata,
|
|
39
|
+
logger,
|
|
40
|
+
apiKey: coingeckoApiKey,
|
|
41
|
+
expirySeconds,
|
|
42
|
+
sleepMsBetweenRequests,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Tries to get the price of a token from CoinGecko. Returns undefined if there's no
|
|
47
|
+
// CoinGecko ID for the token.
|
|
48
|
+
async tryGetTokenPrice(token: Token): Promise<number | undefined> {
|
|
49
|
+
// We only get a price if the token defines a CoinGecko ID.
|
|
50
|
+
// This way we can ignore values of certain types of collateralized warp routes,
|
|
51
|
+
// e.g. Native warp routes on rollups that have been pre-funded.
|
|
52
|
+
const coinGeckoId = token.coinGeckoId;
|
|
53
|
+
|
|
54
|
+
if (!coinGeckoId) {
|
|
55
|
+
this.logger.warn(
|
|
56
|
+
{
|
|
57
|
+
tokenSymbol: token.symbol,
|
|
58
|
+
chain: token.chainName,
|
|
59
|
+
tokenAddress: token.addressOrDenom,
|
|
60
|
+
},
|
|
61
|
+
'CoinGecko ID missing for token',
|
|
62
|
+
);
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return this.getCoingeckoPrice(coinGeckoId);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async getCoingeckoPrice(coingeckoId: string): Promise<number | undefined> {
|
|
70
|
+
const prices = await this.getTokenPriceByIds([coingeckoId]);
|
|
71
|
+
if (!prices) return undefined;
|
|
72
|
+
return prices[0];
|
|
73
|
+
}
|
|
74
|
+
}
|