@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.
Files changed (202) hide show
  1. package/README.md +178 -0
  2. package/dist/config/RebalancerConfig.d.ts +12 -0
  3. package/dist/config/RebalancerConfig.d.ts.map +1 -0
  4. package/dist/config/RebalancerConfig.js +29 -0
  5. package/dist/config/RebalancerConfig.js.map +1 -0
  6. package/dist/config/RebalancerConfig.test.d.ts +2 -0
  7. package/dist/config/RebalancerConfig.test.d.ts.map +1 -0
  8. package/dist/config/RebalancerConfig.test.js +325 -0
  9. package/dist/config/RebalancerConfig.test.js.map +1 -0
  10. package/dist/core/Rebalancer.d.ts +23 -0
  11. package/dist/core/Rebalancer.d.ts.map +1 -0
  12. package/dist/core/Rebalancer.js +290 -0
  13. package/dist/core/Rebalancer.js.map +1 -0
  14. package/dist/core/RebalancerService.d.ts +115 -0
  15. package/dist/core/RebalancerService.d.ts.map +1 -0
  16. package/dist/core/RebalancerService.js +227 -0
  17. package/dist/core/RebalancerService.js.map +1 -0
  18. package/dist/core/WithInflightGuard.d.ts +20 -0
  19. package/dist/core/WithInflightGuard.d.ts.map +1 -0
  20. package/dist/core/WithInflightGuard.js +47 -0
  21. package/dist/core/WithInflightGuard.js.map +1 -0
  22. package/dist/core/WithInflightGuard.test.d.ts +2 -0
  23. package/dist/core/WithInflightGuard.test.d.ts.map +1 -0
  24. package/dist/core/WithInflightGuard.test.js +64 -0
  25. package/dist/core/WithInflightGuard.test.js.map +1 -0
  26. package/dist/core/WithSemaphore.d.ts +22 -0
  27. package/dist/core/WithSemaphore.d.ts.map +1 -0
  28. package/dist/core/WithSemaphore.js +67 -0
  29. package/dist/core/WithSemaphore.js.map +1 -0
  30. package/dist/core/WithSemaphore.test.d.ts +2 -0
  31. package/dist/core/WithSemaphore.test.d.ts.map +1 -0
  32. package/dist/core/WithSemaphore.test.js +83 -0
  33. package/dist/core/WithSemaphore.test.js.map +1 -0
  34. package/dist/factories/RebalancerContextFactory.d.ts +41 -0
  35. package/dist/factories/RebalancerContextFactory.d.ts.map +1 -0
  36. package/dist/factories/RebalancerContextFactory.js +115 -0
  37. package/dist/factories/RebalancerContextFactory.js.map +1 -0
  38. package/dist/index.d.ts +33 -0
  39. package/dist/index.d.ts.map +1 -0
  40. package/dist/index.js +35 -0
  41. package/dist/index.js.map +1 -0
  42. package/dist/interfaces/IMetrics.d.ts +5 -0
  43. package/dist/interfaces/IMetrics.d.ts.map +1 -0
  44. package/dist/interfaces/IMetrics.js +2 -0
  45. package/dist/interfaces/IMetrics.js.map +1 -0
  46. package/dist/interfaces/IMonitor.d.ts +51 -0
  47. package/dist/interfaces/IMonitor.d.ts.map +1 -0
  48. package/dist/interfaces/IMonitor.js +14 -0
  49. package/dist/interfaces/IMonitor.js.map +1 -0
  50. package/dist/interfaces/IRebalancer.d.ts +15 -0
  51. package/dist/interfaces/IRebalancer.d.ts.map +1 -0
  52. package/dist/interfaces/IRebalancer.js +2 -0
  53. package/dist/interfaces/IRebalancer.js.map +1 -0
  54. package/dist/interfaces/IStrategy.d.ts +11 -0
  55. package/dist/interfaces/IStrategy.d.ts.map +1 -0
  56. package/dist/interfaces/IStrategy.js +2 -0
  57. package/dist/interfaces/IStrategy.js.map +1 -0
  58. package/dist/metrics/Metrics.d.ts +31 -0
  59. package/dist/metrics/Metrics.d.ts.map +1 -0
  60. package/dist/metrics/Metrics.js +302 -0
  61. package/dist/metrics/Metrics.js.map +1 -0
  62. package/dist/metrics/PriceGetter.d.ts +10 -0
  63. package/dist/metrics/PriceGetter.d.ts.map +1 -0
  64. package/dist/metrics/PriceGetter.js +41 -0
  65. package/dist/metrics/PriceGetter.js.map +1 -0
  66. package/dist/metrics/scripts/metrics.d.ts +14 -0
  67. package/dist/metrics/scripts/metrics.d.ts.map +1 -0
  68. package/dist/metrics/scripts/metrics.js +198 -0
  69. package/dist/metrics/scripts/metrics.js.map +1 -0
  70. package/dist/metrics/types.d.ts +24 -0
  71. package/dist/metrics/types.d.ts.map +1 -0
  72. package/dist/metrics/types.js +2 -0
  73. package/dist/metrics/types.js.map +1 -0
  74. package/dist/metrics/utils/metrics.d.ts +12 -0
  75. package/dist/metrics/utils/metrics.d.ts.map +1 -0
  76. package/dist/metrics/utils/metrics.js +28 -0
  77. package/dist/metrics/utils/metrics.js.map +1 -0
  78. package/dist/monitor/Monitor.d.ts +26 -0
  79. package/dist/monitor/Monitor.d.ts.map +1 -0
  80. package/dist/monitor/Monitor.js +116 -0
  81. package/dist/monitor/Monitor.js.map +1 -0
  82. package/dist/service.d.ts +3 -0
  83. package/dist/service.d.ts.map +1 -0
  84. package/dist/service.js +125 -0
  85. package/dist/service.js.map +1 -0
  86. package/dist/strategy/BaseStrategy.d.ts +34 -0
  87. package/dist/strategy/BaseStrategy.d.ts.map +1 -0
  88. package/dist/strategy/BaseStrategy.js +127 -0
  89. package/dist/strategy/BaseStrategy.js.map +1 -0
  90. package/dist/strategy/MinAmountStrategy.d.ts +27 -0
  91. package/dist/strategy/MinAmountStrategy.d.ts.map +1 -0
  92. package/dist/strategy/MinAmountStrategy.js +103 -0
  93. package/dist/strategy/MinAmountStrategy.js.map +1 -0
  94. package/dist/strategy/MinAmountStrategy.test.d.ts +2 -0
  95. package/dist/strategy/MinAmountStrategy.test.d.ts.map +1 -0
  96. package/dist/strategy/MinAmountStrategy.test.js +472 -0
  97. package/dist/strategy/MinAmountStrategy.test.js.map +1 -0
  98. package/dist/strategy/StrategyFactory.d.ts +16 -0
  99. package/dist/strategy/StrategyFactory.d.ts.map +1 -0
  100. package/dist/strategy/StrategyFactory.js +25 -0
  101. package/dist/strategy/StrategyFactory.js.map +1 -0
  102. package/dist/strategy/StrategyFactory.test.d.ts +2 -0
  103. package/dist/strategy/StrategyFactory.test.d.ts.map +1 -0
  104. package/dist/strategy/StrategyFactory.test.js +80 -0
  105. package/dist/strategy/StrategyFactory.test.js.map +1 -0
  106. package/dist/strategy/WeightedStrategy.d.ts +23 -0
  107. package/dist/strategy/WeightedStrategy.d.ts.map +1 -0
  108. package/dist/strategy/WeightedStrategy.js +61 -0
  109. package/dist/strategy/WeightedStrategy.js.map +1 -0
  110. package/dist/strategy/WeightedStrategy.test.d.ts +2 -0
  111. package/dist/strategy/WeightedStrategy.test.d.ts.map +1 -0
  112. package/dist/strategy/WeightedStrategy.test.js +307 -0
  113. package/dist/strategy/WeightedStrategy.test.js.map +1 -0
  114. package/dist/strategy/index.d.ts +5 -0
  115. package/dist/strategy/index.d.ts.map +1 -0
  116. package/dist/strategy/index.js +5 -0
  117. package/dist/strategy/index.js.map +1 -0
  118. package/dist/test/helpers.d.ts +8 -0
  119. package/dist/test/helpers.d.ts.map +1 -0
  120. package/dist/test/helpers.js +33 -0
  121. package/dist/test/helpers.js.map +1 -0
  122. package/dist/utils/ExplorerClient.d.ts +14 -0
  123. package/dist/utils/ExplorerClient.d.ts.map +1 -0
  124. package/dist/utils/ExplorerClient.js +82 -0
  125. package/dist/utils/ExplorerClient.js.map +1 -0
  126. package/dist/utils/balanceUtils.d.ts +13 -0
  127. package/dist/utils/balanceUtils.d.ts.map +1 -0
  128. package/dist/utils/balanceUtils.js +43 -0
  129. package/dist/utils/balanceUtils.js.map +1 -0
  130. package/dist/utils/balanceUtils.test.d.ts +2 -0
  131. package/dist/utils/balanceUtils.test.d.ts.map +1 -0
  132. package/dist/utils/balanceUtils.test.js +54 -0
  133. package/dist/utils/balanceUtils.test.js.map +1 -0
  134. package/dist/utils/bridgeUtils.d.ts +19 -0
  135. package/dist/utils/bridgeUtils.d.ts.map +1 -0
  136. package/dist/utils/bridgeUtils.js +20 -0
  137. package/dist/utils/bridgeUtils.js.map +1 -0
  138. package/dist/utils/bridgeUtils.test.d.ts +2 -0
  139. package/dist/utils/bridgeUtils.test.d.ts.map +1 -0
  140. package/dist/utils/bridgeUtils.test.js +77 -0
  141. package/dist/utils/bridgeUtils.test.js.map +1 -0
  142. package/dist/utils/errors.d.ts +4 -0
  143. package/dist/utils/errors.d.ts.map +1 -0
  144. package/dist/utils/errors.js +6 -0
  145. package/dist/utils/errors.js.map +1 -0
  146. package/dist/utils/files.d.ts +35 -0
  147. package/dist/utils/files.d.ts.map +1 -0
  148. package/dist/utils/files.js +190 -0
  149. package/dist/utils/files.js.map +1 -0
  150. package/dist/utils/generalUtils.d.ts +3 -0
  151. package/dist/utils/generalUtils.d.ts.map +1 -0
  152. package/dist/utils/generalUtils.js +9 -0
  153. package/dist/utils/generalUtils.js.map +1 -0
  154. package/dist/utils/index.d.ts +5 -0
  155. package/dist/utils/index.d.ts.map +1 -0
  156. package/dist/utils/index.js +5 -0
  157. package/dist/utils/index.js.map +1 -0
  158. package/dist/utils/tokenUtils.d.ts +14 -0
  159. package/dist/utils/tokenUtils.d.ts.map +1 -0
  160. package/dist/utils/tokenUtils.js +21 -0
  161. package/dist/utils/tokenUtils.js.map +1 -0
  162. package/package.json +70 -0
  163. package/src/config/RebalancerConfig.test.ts +388 -0
  164. package/src/config/RebalancerConfig.ts +39 -0
  165. package/src/core/Rebalancer.ts +471 -0
  166. package/src/core/RebalancerService.ts +333 -0
  167. package/src/core/WithInflightGuard.test.ts +131 -0
  168. package/src/core/WithInflightGuard.ts +67 -0
  169. package/src/core/WithSemaphore.test.ts +112 -0
  170. package/src/core/WithSemaphore.ts +92 -0
  171. package/src/factories/RebalancerContextFactory.ts +210 -0
  172. package/src/index.ts +68 -0
  173. package/src/interfaces/IMetrics.ts +5 -0
  174. package/src/interfaces/IMonitor.ts +63 -0
  175. package/src/interfaces/IRebalancer.ts +20 -0
  176. package/src/interfaces/IStrategy.ts +13 -0
  177. package/src/metrics/Metrics.ts +558 -0
  178. package/src/metrics/PriceGetter.ts +74 -0
  179. package/src/metrics/scripts/metrics.ts +298 -0
  180. package/src/metrics/types.ts +27 -0
  181. package/src/metrics/utils/metrics.ts +33 -0
  182. package/src/monitor/Monitor.ts +174 -0
  183. package/src/service.ts +154 -0
  184. package/src/strategy/BaseStrategy.ts +210 -0
  185. package/src/strategy/MinAmountStrategy.test.ts +625 -0
  186. package/src/strategy/MinAmountStrategy.ts +170 -0
  187. package/src/strategy/StrategyFactory.test.ts +109 -0
  188. package/src/strategy/StrategyFactory.ts +48 -0
  189. package/src/strategy/WeightedStrategy.test.ts +408 -0
  190. package/src/strategy/WeightedStrategy.ts +93 -0
  191. package/src/strategy/index.ts +4 -0
  192. package/src/test/helpers.ts +46 -0
  193. package/src/utils/ExplorerClient.ts +99 -0
  194. package/src/utils/balanceUtils.test.ts +74 -0
  195. package/src/utils/balanceUtils.ts +69 -0
  196. package/src/utils/bridgeUtils.test.ts +92 -0
  197. package/src/utils/bridgeUtils.ts +42 -0
  198. package/src/utils/errors.ts +5 -0
  199. package/src/utils/files.ts +276 -0
  200. package/src/utils/generalUtils.ts +13 -0
  201. package/src/utils/index.ts +4 -0
  202. package/src/utils/tokenUtils.ts +26 -0
@@ -0,0 +1,74 @@
1
+ import { expect } from 'chai';
2
+ import { pino } from 'pino';
3
+ import sinon from 'sinon';
4
+
5
+ import { ChainName, Token, TokenStandard } from '@hyperlane-xyz/sdk';
6
+
7
+ import { MonitorEvent } from '../interfaces/IMonitor.js';
8
+
9
+ import { getRawBalances } from './balanceUtils.js';
10
+
11
+ const testLogger = pino({ level: 'silent' });
12
+
13
+ describe('getRawBalances', () => {
14
+ let chains: ChainName[];
15
+ let token: Token;
16
+ let tokenBridgedSupply: bigint;
17
+ let event: MonitorEvent;
18
+
19
+ beforeEach(() => {
20
+ chains = ['mainnet'];
21
+
22
+ token = {
23
+ chainName: 'mainnet',
24
+ addressOrDenom: '0xAddress',
25
+ isCollateralized: sinon.stub().returns(true),
26
+ standard: TokenStandard.EvmHypCollateral,
27
+ } as unknown as Token;
28
+
29
+ tokenBridgedSupply = 100n;
30
+
31
+ event = {
32
+ tokensInfo: [
33
+ {
34
+ token,
35
+ bridgedSupply: tokenBridgedSupply,
36
+ },
37
+ ],
38
+ };
39
+ });
40
+
41
+ it('should return the bridged supply for the token (EvmHypCollateral)', () => {
42
+ expect(getRawBalances(chains, event, testLogger)).to.deep.equal({
43
+ mainnet: tokenBridgedSupply,
44
+ });
45
+ });
46
+
47
+ it('should return the bridged supply for the token (EvmHypNative)', () => {
48
+ token.standard = TokenStandard.EvmHypNative;
49
+
50
+ expect(getRawBalances(chains, event, testLogger)).to.deep.equal({
51
+ mainnet: tokenBridgedSupply,
52
+ });
53
+ });
54
+
55
+ it('should ignore non supported token standards', () => {
56
+ token.standard = TokenStandard.EvmHypOwnerCollateral;
57
+
58
+ expect(getRawBalances(chains, event, testLogger)).to.deep.equal({});
59
+ });
60
+
61
+ it('should ignore tokens that are not in the chains list', () => {
62
+ chains = [];
63
+
64
+ expect(getRawBalances(chains, event, testLogger)).to.deep.equal({});
65
+ });
66
+
67
+ it('should throw if the bridged supply is undefined', () => {
68
+ delete event.tokensInfo[0].bridgedSupply;
69
+
70
+ expect(() => getRawBalances(chains, event, testLogger)).to.throw(
71
+ 'bridgedSupply should not be undefined for collateralized token 0xAddress',
72
+ );
73
+ });
74
+ });
@@ -0,0 +1,69 @@
1
+ import { Logger } from 'pino';
2
+
3
+ import { ChainName, Token } from '@hyperlane-xyz/sdk';
4
+
5
+ import { MonitorEvent } from '../interfaces/IMonitor.js';
6
+ import { RawBalances } from '../interfaces/IStrategy.js';
7
+
8
+ import { isCollateralizedTokenEligibleForRebalancing } from './tokenUtils.js';
9
+
10
+ /**
11
+ * Returns the raw balances required by the strategies from the monitor event
12
+ * @param chains - The chains that should be included in the raw balances (e.g. the chains in the rebalancer config)
13
+ * @param event - The monitor event to extract the raw balances from
14
+ * @returns An object mapping chain names to their raw balances.
15
+ */
16
+ export function getRawBalances(
17
+ chains: ChainName[],
18
+ event: MonitorEvent,
19
+ logger: Logger,
20
+ ): RawBalances {
21
+ const balances: RawBalances = {};
22
+ const chainSet = new Set(chains);
23
+
24
+ for (const tokenInfo of event.tokensInfo) {
25
+ const { token, bridgedSupply } = tokenInfo;
26
+
27
+ // Ignore tokens that are not in the provided chains list
28
+ if (!chainSet.has(token.chainName)) {
29
+ logger.info(
30
+ {
31
+ context: getRawBalances.name,
32
+ chain: token.chainName,
33
+ tokenSymbol: token.symbol,
34
+ tokenAddress: token.addressOrDenom,
35
+ },
36
+ 'Skipping token: not in configured chains list',
37
+ );
38
+ continue;
39
+ }
40
+
41
+ // Ignore tokens that are not collateralized or are otherwise ineligible
42
+ if (!isCollateralizedTokenEligibleForRebalancing(token)) {
43
+ logger.info(
44
+ {
45
+ context: getRawBalances.name,
46
+ chain: token.chainName,
47
+ tokenSymbol: token.symbol,
48
+ tokenAddress: token.addressOrDenom,
49
+ },
50
+ 'Skipping token: not collateralized or ineligible for rebalancing',
51
+ );
52
+ continue;
53
+ }
54
+
55
+ if (bridgedSupply === undefined) {
56
+ throw new Error(
57
+ `bridgedSupply should not be undefined for collateralized token ${token.addressOrDenom}`,
58
+ );
59
+ }
60
+
61
+ balances[token.chainName] = bridgedSupply;
62
+ }
63
+
64
+ return balances;
65
+ }
66
+
67
+ export function formatBigInt(warpToken: Token, num: bigint): number {
68
+ return warpToken.amount(num).getDecimalFormattedAmount();
69
+ }
@@ -0,0 +1,92 @@
1
+ import { expect } from 'chai';
2
+ import { pino } from 'pino';
3
+
4
+ import { ChainMap } from '@hyperlane-xyz/sdk';
5
+
6
+ import {
7
+ type BridgeConfigWithOverride,
8
+ getBridgeConfig,
9
+ } from './bridgeUtils.js';
10
+
11
+ const testLogger = pino({ level: 'silent' });
12
+
13
+ describe('bridgeConfig', () => {
14
+ it('should return the base bridge config when no overrides exist', () => {
15
+ const bridges: ChainMap<BridgeConfigWithOverride> = {
16
+ chain1: {
17
+ bridge: '0x1234567890123456789012345678901234567890',
18
+ bridgeMinAcceptedAmount: 1000,
19
+ bridgeIsWarp: true,
20
+ },
21
+ chain2: {
22
+ bridge: '0x0987654321098765432109876543210987654321',
23
+ bridgeMinAcceptedAmount: 2000,
24
+ bridgeIsWarp: true,
25
+ },
26
+ };
27
+
28
+ const result = getBridgeConfig(bridges, 'chain1', 'chain2', testLogger);
29
+
30
+ expect(result).to.deep.equal({
31
+ bridge: '0x1234567890123456789012345678901234567890',
32
+ bridgeMinAcceptedAmount: 1000,
33
+ bridgeIsWarp: true,
34
+ });
35
+ });
36
+
37
+ it('should merge base config with overrides when they exist', () => {
38
+ const bridges: ChainMap<BridgeConfigWithOverride> = {
39
+ chain1: {
40
+ bridge: '0x1234567890123456789012345678901234567890',
41
+ bridgeMinAcceptedAmount: 1000,
42
+ bridgeIsWarp: true,
43
+ override: {
44
+ chain2: {
45
+ bridgeMinAcceptedAmount: 5000,
46
+ },
47
+ },
48
+ },
49
+ chain2: {
50
+ bridge: '0x0987654321098765432109876543210987654321',
51
+ bridgeMinAcceptedAmount: 2000,
52
+ bridgeIsWarp: true,
53
+ },
54
+ };
55
+
56
+ const result = getBridgeConfig(bridges, 'chain1', 'chain2', testLogger);
57
+
58
+ expect(result).to.deep.equal({
59
+ bridge: '0x1234567890123456789012345678901234567890',
60
+ bridgeMinAcceptedAmount: 5000,
61
+ bridgeIsWarp: true,
62
+ });
63
+ });
64
+
65
+ it('should handle overrides that change the bridge address', () => {
66
+ const bridges: ChainMap<BridgeConfigWithOverride> = {
67
+ chain1: {
68
+ bridge: '0x1234567890123456789012345678901234567890',
69
+ bridgeMinAcceptedAmount: 1000,
70
+ bridgeIsWarp: true,
71
+ override: {
72
+ chain2: {
73
+ bridge: '0xABCDEF0123456789ABCDEF0123456789ABCDEF01',
74
+ },
75
+ },
76
+ },
77
+ chain2: {
78
+ bridge: '0x0987654321098765432109876543210987654321',
79
+ bridgeMinAcceptedAmount: 2000,
80
+ bridgeIsWarp: true,
81
+ },
82
+ };
83
+
84
+ const result = getBridgeConfig(bridges, 'chain1', 'chain2', testLogger);
85
+
86
+ expect(result).to.deep.equal({
87
+ bridge: '0xABCDEF0123456789ABCDEF0123456789ABCDEF01',
88
+ bridgeMinAcceptedAmount: 1000,
89
+ bridgeIsWarp: true,
90
+ });
91
+ });
92
+ });
@@ -0,0 +1,42 @@
1
+ import { Logger } from 'pino';
2
+
3
+ import type { ChainMap, ChainName } from '@hyperlane-xyz/sdk';
4
+
5
+ export type BridgeConfigWithOverride = BridgeConfig & {
6
+ override?: ChainMap<Partial<BridgeConfig>>;
7
+ };
8
+
9
+ export type BridgeConfig = {
10
+ bridge: string;
11
+ bridgeMinAcceptedAmount: string | number;
12
+ bridgeIsWarp: boolean;
13
+ };
14
+
15
+ /**
16
+ * Gets the bridge configuration for a specific chain pair, applying any overrides
17
+ * @param bridges The map of bridge configurations by chain
18
+ * @param fromChain The source chain
19
+ * @param toChain The destination chain
20
+ * @returns The bridge configuration with any overrides applied
21
+ */
22
+ export function getBridgeConfig(
23
+ bridges: ChainMap<BridgeConfigWithOverride>,
24
+ fromChain: ChainName,
25
+ toChain: ChainName,
26
+ logger: Logger,
27
+ ): BridgeConfig {
28
+ const fromConfig = bridges[fromChain];
29
+
30
+ if (!fromConfig) {
31
+ logger.error({ fromChain }, 'Bridge config not found');
32
+ throw new Error(`Bridge config not found for chain ${fromChain}`);
33
+ }
34
+
35
+ const routeSpecificOverrides = fromConfig.override?.[toChain];
36
+
37
+ // Create a new object with the properties from bridgeConfig, excluding the overrides property
38
+ const { override: _, ...baseConfig } = fromConfig;
39
+
40
+ // Return a new object with the base config and any overrides
41
+ return { ...baseConfig, ...routeSpecificOverrides };
42
+ }
@@ -0,0 +1,5 @@
1
+ export class WrappedError extends Error {
2
+ constructor(message: string, cause?: Error) {
3
+ super(message, cause ? { cause } : undefined);
4
+ }
5
+ }
@@ -0,0 +1,276 @@
1
+ import { input } from '@inquirer/prompts';
2
+ import select from '@inquirer/select';
3
+ import fs from 'fs';
4
+ import os from 'os';
5
+ import path from 'path';
6
+ import {
7
+ DocumentOptions,
8
+ LineCounter,
9
+ ParseOptions,
10
+ SchemaOptions,
11
+ ToJSOptions,
12
+ parse,
13
+ stringify as yamlStringify,
14
+ } from 'yaml';
15
+
16
+ import { objMerge, rootLogger } from '@hyperlane-xyz/utils';
17
+
18
+ const yamlParse = (
19
+ content: string,
20
+ options?: ParseOptions & DocumentOptions & SchemaOptions & ToJSOptions,
21
+ ) =>
22
+ // See stackoverflow.com/questions/63075256/why-does-the-npm-yaml-library-have-a-max-alias-number
23
+ parse(content, { maxAliasCount: -1, ...options });
24
+
25
+ export const MAX_READ_LINE_OUTPUT = 250;
26
+
27
+ export type FileFormat = 'yaml' | 'json';
28
+
29
+ export type ArtifactsFile = {
30
+ filename: string;
31
+ description: string;
32
+ };
33
+
34
+ export function removeEndingSlash(dirPath: string): string {
35
+ if (dirPath.endsWith('/')) {
36
+ return dirPath.slice(0, -1);
37
+ }
38
+ return dirPath;
39
+ }
40
+
41
+ export function resolvePath(filePath: string): string {
42
+ if (filePath.startsWith('~')) {
43
+ const homedir = os.homedir();
44
+ return path.join(homedir, filePath.slice(1));
45
+ }
46
+ return filePath;
47
+ }
48
+
49
+ export function isFile(filepath: string): boolean {
50
+ if (!filepath) return false;
51
+ try {
52
+ return fs.existsSync(filepath) && fs.lstatSync(filepath).isFile();
53
+ } catch {
54
+ rootLogger.warn(`Error checking for file: ${filepath}`);
55
+ return false;
56
+ }
57
+ }
58
+
59
+ export function isDirectory(dirpath: string): boolean {
60
+ if (!dirpath) return false;
61
+ try {
62
+ return fs.existsSync(dirpath) && fs.lstatSync(dirpath).isDirectory();
63
+ } catch {
64
+ rootLogger.warn(`Error checking for directory: ${dirpath}`);
65
+ return false;
66
+ }
67
+ }
68
+
69
+ export function readFileAtPath(filepath: string): string {
70
+ if (!isFile(filepath)) {
71
+ throw Error(`File doesn't exist at ${filepath}`);
72
+ }
73
+ return fs.readFileSync(filepath, 'utf8');
74
+ }
75
+
76
+ export function writeFileAtPath(filepath: string, value: string): void {
77
+ const dirname = path.dirname(filepath);
78
+ if (!isDirectory(dirname)) {
79
+ fs.mkdirSync(dirname, { recursive: true });
80
+ }
81
+ fs.writeFileSync(filepath, value);
82
+ }
83
+
84
+ export function readJson<T>(filepath: string): T {
85
+ return JSON.parse(readFileAtPath(filepath)) as T;
86
+ }
87
+
88
+ export function tryReadJson<T>(filepath: string): T | null {
89
+ try {
90
+ return readJson(filepath) as T;
91
+ } catch {
92
+ return null;
93
+ }
94
+ }
95
+
96
+ export function writeJson(filepath: string, obj: any) {
97
+ writeFileAtPath(filepath, JSON.stringify(obj, null, 2) + '\n');
98
+ }
99
+
100
+ export function mergeJson<T extends Record<string, any>>(
101
+ filepath: string,
102
+ obj: T,
103
+ ) {
104
+ if (isFile(filepath)) {
105
+ const previous = readJson<T>(filepath);
106
+ writeJson(filepath, objMerge(previous, obj));
107
+ } else {
108
+ writeJson(filepath, obj);
109
+ }
110
+ }
111
+
112
+ export function readYaml<T>(filepath: string): T {
113
+ return yamlParse(readFileAtPath(filepath)) as T;
114
+ }
115
+
116
+ export function tryReadYamlAtPath<T>(filepath: string): T | null {
117
+ try {
118
+ return readYaml(filepath);
119
+ } catch {
120
+ return null;
121
+ }
122
+ }
123
+
124
+ export function writeYaml(filepath: string, obj: any) {
125
+ writeFileAtPath(
126
+ filepath,
127
+ yamlStringify(obj, { indent: 2, sortMapEntries: true }) + '\n',
128
+ );
129
+ }
130
+
131
+ export function mergeYaml<T extends Record<string, any>>(
132
+ filepath: string,
133
+ obj: T,
134
+ ) {
135
+ if (isFile(filepath)) {
136
+ const previous = readYaml<T>(filepath);
137
+ writeYaml(filepath, objMerge(previous, obj));
138
+ } else {
139
+ writeYaml(filepath, obj);
140
+ }
141
+ }
142
+
143
+ export function readYamlOrJson<T>(filepath: string, format?: FileFormat): T {
144
+ return resolveYamlOrJsonFn(filepath, readJson, readYaml, format);
145
+ }
146
+
147
+ export function writeYamlOrJson(
148
+ filepath: string,
149
+ obj: Record<string, any>,
150
+ format?: FileFormat,
151
+ ) {
152
+ return resolveYamlOrJsonFn(
153
+ filepath,
154
+ (f: string) => writeJson(f, obj),
155
+ (f: string) => writeYaml(f, obj),
156
+ format,
157
+ );
158
+ }
159
+
160
+ export function mergeYamlOrJson(
161
+ filepath: string,
162
+ obj: Record<string, any>,
163
+ format: FileFormat = 'yaml',
164
+ ) {
165
+ return resolveYamlOrJsonFn(
166
+ filepath,
167
+ (f: string) => mergeJson(f, obj),
168
+ (f: string) => mergeYaml(f, obj),
169
+ format,
170
+ );
171
+ }
172
+
173
+ function resolveYamlOrJsonFn(
174
+ filepath: string,
175
+ jsonFn: any,
176
+ yamlFn: any,
177
+ format?: FileFormat,
178
+ ) {
179
+ const fileFormat = resolveFileFormat(filepath, format);
180
+ if (!fileFormat) {
181
+ throw new Error(`Invalid file format for ${filepath}`);
182
+ }
183
+
184
+ if (fileFormat === 'json') {
185
+ return jsonFn(filepath);
186
+ }
187
+
188
+ return yamlFn(filepath);
189
+ }
190
+
191
+ export function resolveFileFormat(
192
+ filepath?: string,
193
+ format?: FileFormat,
194
+ ): FileFormat | undefined {
195
+ // early out if filepath is undefined
196
+ if (!filepath) {
197
+ return format;
198
+ }
199
+
200
+ if (format === 'json' || filepath?.endsWith('.json')) {
201
+ return 'json';
202
+ }
203
+
204
+ if (
205
+ format === 'yaml' ||
206
+ filepath?.endsWith('.yaml') ||
207
+ filepath?.endsWith('.yml')
208
+ ) {
209
+ return 'yaml';
210
+ }
211
+
212
+ return undefined;
213
+ }
214
+
215
+ export async function runFileSelectionStep(
216
+ folderPath: string,
217
+ description: string,
218
+ pattern?: string,
219
+ ) {
220
+ const noFilesErrorMessage = `No "${description}" found in ${folderPath}. Please confirm the path for "${description}". By default, the CLI writes to folders relative to where its run.`;
221
+ if (!fs.existsSync(folderPath)) throw new Error(noFilesErrorMessage);
222
+
223
+ let filenames = fs.readdirSync(folderPath);
224
+ if (pattern) {
225
+ filenames = filenames.filter((f) => f.includes(pattern));
226
+ }
227
+
228
+ if (filenames.length === 0) throw new Error(noFilesErrorMessage);
229
+
230
+ let filename = (await select({
231
+ message: `Select ${description} file`,
232
+ choices: [
233
+ ...filenames.map((f) => ({ name: f, value: f })),
234
+ { name: '(Other file)', value: null },
235
+ ],
236
+ pageSize: 20,
237
+ })) as string;
238
+
239
+ if (filename) return path.join(folderPath, filename);
240
+
241
+ filename = await input({
242
+ message: `Enter ${description} filepath`,
243
+ });
244
+
245
+ if (filename) return filename;
246
+ else throw new Error(`No filepath entered ${description}`);
247
+ }
248
+
249
+ export function indentYamlOrJson(str: string, indentLevel: number): string {
250
+ const indent = ' '.repeat(indentLevel);
251
+ return str
252
+ .split('\n')
253
+ .map((line) => indent + line)
254
+ .join('\n');
255
+ }
256
+
257
+ /**
258
+ * Logs the YAML representation of an object if the number of lines is less than the specified maximum.
259
+ *
260
+ * @param obj - The object to be converted to YAML.
261
+ * @param maxLines - The maximum number of lines allowed for the YAML representation.
262
+ * @param margin - The number of spaces to use for indentation (default is 2).
263
+ */
264
+ export function logYamlIfUnderMaxLines(
265
+ obj: any,
266
+ maxLines: number = MAX_READ_LINE_OUTPUT,
267
+ margin: number = 2,
268
+ ): void {
269
+ const asYamlString = yamlStringify(obj, null, margin);
270
+ const lineCounter = new LineCounter();
271
+ yamlParse(asYamlString, { lineCounter });
272
+
273
+ if (lineCounter.lineStarts.length < maxLines) {
274
+ rootLogger.info(asYamlString);
275
+ }
276
+ }
@@ -0,0 +1,13 @@
1
+ import { Logger } from 'pino';
2
+
3
+ export async function tryFn(
4
+ fn: () => Promise<void>,
5
+ context: string,
6
+ logger: Logger,
7
+ ) {
8
+ try {
9
+ await fn();
10
+ } catch (error) {
11
+ logger.error({ context, err: error as Error }, `Error in ${context}`);
12
+ }
13
+ }
@@ -0,0 +1,4 @@
1
+ export * from './balanceUtils.js';
2
+ export * from './bridgeUtils.js';
3
+ export * from './generalUtils.js';
4
+ export * from './tokenUtils.js';
@@ -0,0 +1,26 @@
1
+ import { Token, TokenStandard } from '@hyperlane-xyz/sdk';
2
+
3
+ const REBALANCEABLE_TOKEN_COLLATERALIZED_STANDARDS = new Set<TokenStandard>([
4
+ TokenStandard.EvmHypCollateral,
5
+ TokenStandard.EvmHypNative,
6
+ ]);
7
+
8
+ /**
9
+ * @dev This function exists because the rebalancer currently only supports a subset of collateralized token standards
10
+ * (see `REBALANCEABLE_TOKEN_COLLATERALIZED_STANDARDS` vs. all possible `TOKEN_COLLATERALIZED_STANDARDS`).
11
+ *
12
+ * @deprecated This function is conditionally deprecated. It is intended for removal once the rebalancer
13
+ * achieves full support for all collateralized token standards.
14
+ * After this condition is met and this function is removed, please use `token.isCollateralized()` as the alternative.
15
+ *
16
+ * @param token - The token to be checked for rebalancing eligibility.
17
+ * @returns `true` if the token is of a collateralized standard currently supported by the rebalancer, `false` otherwise.
18
+ */
19
+ export function isCollateralizedTokenEligibleForRebalancing(
20
+ token: Token,
21
+ ): boolean {
22
+ return (
23
+ token.isCollateralized() &&
24
+ REBALANCEABLE_TOKEN_COLLATERALIZED_STANDARDS.has(token.standard)
25
+ );
26
+ }