@humanode-hyperlane/rebalancer 27.2.2-humanode.1 → 1029.1.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/dist/bridges/LiFiBridge.d.ts +3 -3
- package/dist/bridges/LiFiBridge.d.ts.map +1 -1
- package/dist/bridges/LiFiBridge.js +66 -55
- package/dist/bridges/LiFiBridge.js.map +1 -1
- package/dist/bridges/LiFiBridge.test.js +213 -0
- package/dist/bridges/LiFiBridge.test.js.map +1 -1
- package/dist/config/RebalancerConfig.d.ts +4 -3
- package/dist/config/RebalancerConfig.d.ts.map +1 -1
- package/dist/config/RebalancerConfig.js +7 -5
- package/dist/config/RebalancerConfig.js.map +1 -1
- package/dist/config/RebalancerConfig.test.js +278 -2
- package/dist/config/RebalancerConfig.test.js.map +1 -1
- package/dist/config/types.d.ts +707 -2664
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/types.js +62 -7
- package/dist/config/types.js.map +1 -1
- package/dist/core/InventoryRebalancer.d.ts +13 -19
- package/dist/core/InventoryRebalancer.d.ts.map +1 -1
- package/dist/core/InventoryRebalancer.js +404 -276
- package/dist/core/InventoryRebalancer.js.map +1 -1
- package/dist/core/InventoryRebalancer.test.js +814 -25
- package/dist/core/InventoryRebalancer.test.js.map +1 -1
- package/dist/core/Rebalancer.d.ts.map +1 -1
- package/dist/core/Rebalancer.js +12 -6
- package/dist/core/Rebalancer.js.map +1 -1
- package/dist/core/Rebalancer.test.js +51 -0
- package/dist/core/Rebalancer.test.js.map +1 -1
- package/dist/core/RebalancerOrchestrator.d.ts.map +1 -1
- package/dist/core/RebalancerOrchestrator.test.js +0 -1
- package/dist/core/RebalancerOrchestrator.test.js.map +1 -1
- package/dist/core/RebalancerService.d.ts.map +1 -1
- package/dist/core/RebalancerService.js +3 -2
- package/dist/core/RebalancerService.js.map +1 -1
- package/dist/core/RebalancerService.test.js +24 -0
- package/dist/core/RebalancerService.test.js.map +1 -1
- package/dist/e2e/fixtures/routes.d.ts +3 -3
- package/dist/e2e/fixtures/routes.d.ts.map +1 -1
- package/dist/e2e/harness/BaseLocalDeploymentManager.d.ts.map +1 -1
- package/dist/e2e/harness/BaseLocalDeploymentManager.js +3 -2
- package/dist/e2e/harness/BaseLocalDeploymentManager.js.map +1 -1
- package/dist/e2e/harness/Erc20InventoryLocalDeploymentManager.d.ts.map +1 -1
- package/dist/e2e/harness/Erc20InventoryLocalDeploymentManager.js.map +1 -1
- package/dist/e2e/harness/Erc20LocalDeploymentManager.d.ts.map +1 -1
- package/dist/e2e/harness/ForkIndexer.d.ts.map +1 -1
- package/dist/e2e/harness/ForkIndexer.js.map +1 -1
- package/dist/e2e/harness/LocalProvider.d.ts +3 -0
- package/dist/e2e/harness/LocalProvider.d.ts.map +1 -0
- package/dist/e2e/harness/LocalProvider.js +9 -0
- package/dist/e2e/harness/LocalProvider.js.map +1 -0
- package/dist/e2e/harness/LocalProvider.test.d.ts +2 -0
- package/dist/e2e/harness/LocalProvider.test.d.ts.map +1 -0
- package/dist/e2e/harness/LocalProvider.test.js +14 -0
- package/dist/e2e/harness/LocalProvider.test.js.map +1 -0
- package/dist/e2e/harness/MixedLocalDeploymentManager.d.ts.map +1 -1
- package/dist/e2e/harness/MixedLocalDeploymentManager.js.map +1 -1
- package/dist/e2e/harness/MockExplorerClient.d.ts.map +1 -1
- package/dist/e2e/harness/MockExplorerClient.js.map +1 -1
- package/dist/e2e/harness/MockExternalBridge.d.ts.map +1 -1
- package/dist/e2e/harness/MockExternalBridge.js.map +1 -1
- package/dist/e2e/harness/NativeLocalDeploymentManager.d.ts.map +1 -1
- package/dist/e2e/harness/NativeLocalDeploymentManager.js +2 -1
- package/dist/e2e/harness/NativeLocalDeploymentManager.js.map +1 -1
- package/dist/e2e/harness/SnapshotHelper.d.ts.map +1 -1
- package/dist/e2e/harness/SnapshotHelper.js +3 -5
- package/dist/e2e/harness/SnapshotHelper.js.map +1 -1
- package/dist/e2e/harness/TestHelpers.js +1 -2
- package/dist/e2e/harness/TestHelpers.js.map +1 -1
- package/dist/e2e/harness/TestRebalancer.d.ts +5 -1
- package/dist/e2e/harness/TestRebalancer.d.ts.map +1 -1
- package/dist/e2e/harness/TestRebalancer.js +14 -4
- package/dist/e2e/harness/TestRebalancer.js.map +1 -1
- package/dist/e2e/relayer-denylist.e2e-test.d.ts +2 -0
- package/dist/e2e/relayer-denylist.e2e-test.d.ts.map +1 -0
- package/dist/e2e/relayer-denylist.e2e-test.js +388 -0
- package/dist/e2e/relayer-denylist.e2e-test.js.map +1 -0
- package/dist/factories/RebalancerContextFactory.d.ts.map +1 -1
- package/dist/factories/RebalancerContextFactory.js +61 -31
- package/dist/factories/RebalancerContextFactory.js.map +1 -1
- package/dist/factories/RebalancerContextFactory.test.js +336 -5
- package/dist/factories/RebalancerContextFactory.test.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/interfaces/IRebalancer.d.ts +4 -2
- package/dist/interfaces/IRebalancer.d.ts.map +1 -1
- package/dist/metrics/Metrics.d.ts.map +1 -1
- package/dist/metrics/Metrics.js +1 -1
- package/dist/metrics/Metrics.js.map +1 -1
- package/dist/metrics/PriceGetter.d.ts +1 -0
- package/dist/metrics/PriceGetter.d.ts.map +1 -1
- package/dist/metrics/PriceGetter.js +14 -4
- package/dist/metrics/PriceGetter.js.map +1 -1
- package/dist/metrics/PriceGetter.test.d.ts +2 -0
- package/dist/metrics/PriceGetter.test.d.ts.map +1 -0
- package/dist/metrics/PriceGetter.test.js +84 -0
- package/dist/metrics/PriceGetter.test.js.map +1 -0
- package/dist/metrics/scripts/metrics.d.ts +4 -4
- package/dist/monitor/Monitor.d.ts +1 -0
- package/dist/monitor/Monitor.d.ts.map +1 -1
- package/dist/monitor/Monitor.js +55 -46
- package/dist/monitor/Monitor.js.map +1 -1
- package/dist/monitor/Monitor.test.d.ts +2 -0
- package/dist/monitor/Monitor.test.d.ts.map +1 -0
- package/dist/monitor/Monitor.test.js +346 -0
- package/dist/monitor/Monitor.test.js.map +1 -0
- package/dist/service.d.ts +1 -1
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +9 -5
- package/dist/service.js.map +1 -1
- package/dist/strategy/BaseStrategy.d.ts.map +1 -1
- package/dist/strategy/BaseStrategy.js +13 -11
- package/dist/strategy/BaseStrategy.js.map +1 -1
- package/dist/strategy/CollateralDeficitStrategy.d.ts.map +1 -1
- package/dist/strategy/CollateralDeficitStrategy.js +2 -2
- package/dist/strategy/CollateralDeficitStrategy.js.map +1 -1
- package/dist/strategy/CompositeStrategy.d.ts.map +1 -1
- package/dist/strategy/CompositeStrategy.js.map +1 -1
- package/dist/strategy/CompositeStrategy.test.js.map +1 -1
- package/dist/strategy/MinAmountStrategy.d.ts +1 -0
- package/dist/strategy/MinAmountStrategy.d.ts.map +1 -1
- package/dist/strategy/MinAmountStrategy.js +12 -8
- package/dist/strategy/MinAmountStrategy.js.map +1 -1
- package/dist/strategy/MinAmountStrategy.test.js +189 -2
- package/dist/strategy/MinAmountStrategy.test.js.map +1 -1
- package/dist/strategy/StrategyFactory.d.ts.map +1 -1
- package/dist/strategy/StrategyFactory.js.map +1 -1
- package/dist/strategy/WeightedStrategy.d.ts.map +1 -1
- package/dist/test/helpers.d.ts +11 -3
- package/dist/test/helpers.d.ts.map +1 -1
- package/dist/test/helpers.js +9 -11
- package/dist/test/helpers.js.map +1 -1
- package/dist/test/lifiMocks.d.ts +6 -6
- package/dist/test/lifiMocks.d.ts.map +1 -1
- package/dist/test/lifiMocks.js +5 -2
- package/dist/test/lifiMocks.js.map +1 -1
- package/dist/test/pendingTransferMiddlewareMocks.d.ts +17 -0
- package/dist/test/pendingTransferMiddlewareMocks.d.ts.map +1 -0
- package/dist/test/pendingTransferMiddlewareMocks.js +43 -0
- package/dist/test/pendingTransferMiddlewareMocks.js.map +1 -0
- package/dist/tracking/ActionTracker.d.ts +4 -0
- package/dist/tracking/ActionTracker.d.ts.map +1 -1
- package/dist/tracking/ActionTracker.js +76 -9
- package/dist/tracking/ActionTracker.js.map +1 -1
- package/dist/tracking/ActionTracker.test.js +418 -1
- package/dist/tracking/ActionTracker.test.js.map +1 -1
- package/dist/tracking/IActionTracker.d.ts +4 -2
- package/dist/tracking/IActionTracker.d.ts.map +1 -1
- package/dist/tracking/InflightContextAdapter.d.ts.map +1 -1
- package/dist/tracking/InflightContextAdapter.js.map +1 -1
- package/dist/tracking/pendingTransferMiddlewares/BlacklistIsmMiddleware.d.ts +64 -0
- package/dist/tracking/pendingTransferMiddlewares/BlacklistIsmMiddleware.d.ts.map +1 -0
- package/dist/tracking/pendingTransferMiddlewares/BlacklistIsmMiddleware.js +474 -0
- package/dist/tracking/pendingTransferMiddlewares/BlacklistIsmMiddleware.js.map +1 -0
- package/dist/tracking/pendingTransferMiddlewares/BlacklistIsmMiddleware.test.d.ts +2 -0
- package/dist/tracking/pendingTransferMiddlewares/BlacklistIsmMiddleware.test.d.ts.map +1 -0
- package/dist/tracking/pendingTransferMiddlewares/BlacklistIsmMiddleware.test.js +1168 -0
- package/dist/tracking/pendingTransferMiddlewares/BlacklistIsmMiddleware.test.js.map +1 -0
- package/dist/tracking/pendingTransferMiddlewares/PendingTransferMiddleware.d.ts +61 -0
- package/dist/tracking/pendingTransferMiddlewares/PendingTransferMiddleware.d.ts.map +1 -0
- package/dist/tracking/pendingTransferMiddlewares/PendingTransferMiddleware.js +160 -0
- package/dist/tracking/pendingTransferMiddlewares/PendingTransferMiddleware.js.map +1 -0
- package/dist/tracking/pendingTransferMiddlewares/PendingTransferMiddleware.test.d.ts +2 -0
- package/dist/tracking/pendingTransferMiddlewares/PendingTransferMiddleware.test.d.ts.map +1 -0
- package/dist/tracking/pendingTransferMiddlewares/PendingTransferMiddleware.test.js +369 -0
- package/dist/tracking/pendingTransferMiddlewares/PendingTransferMiddleware.test.js.map +1 -0
- package/dist/tracking/pendingTransferMiddlewares/RelayerDenylistMiddleware.d.ts +60 -0
- package/dist/tracking/pendingTransferMiddlewares/RelayerDenylistMiddleware.d.ts.map +1 -0
- package/dist/tracking/pendingTransferMiddlewares/RelayerDenylistMiddleware.js +288 -0
- package/dist/tracking/pendingTransferMiddlewares/RelayerDenylistMiddleware.js.map +1 -0
- package/dist/tracking/pendingTransferMiddlewares/RelayerDenylistMiddleware.test.d.ts +2 -0
- package/dist/tracking/pendingTransferMiddlewares/RelayerDenylistMiddleware.test.d.ts.map +1 -0
- package/dist/tracking/pendingTransferMiddlewares/RelayerDenylistMiddleware.test.js +606 -0
- package/dist/tracking/pendingTransferMiddlewares/RelayerDenylistMiddleware.test.js.map +1 -0
- package/dist/tracking/pendingTransferMiddlewares/buildPendingTransferMiddlewares.d.ts +25 -0
- package/dist/tracking/pendingTransferMiddlewares/buildPendingTransferMiddlewares.d.ts.map +1 -0
- package/dist/tracking/pendingTransferMiddlewares/buildPendingTransferMiddlewares.js +41 -0
- package/dist/tracking/pendingTransferMiddlewares/buildPendingTransferMiddlewares.js.map +1 -0
- package/dist/tracking/pendingTransferMiddlewares/buildPendingTransferMiddlewares.test.d.ts +2 -0
- package/dist/tracking/pendingTransferMiddlewares/buildPendingTransferMiddlewares.test.d.ts.map +1 -0
- package/dist/tracking/pendingTransferMiddlewares/buildPendingTransferMiddlewares.test.js +141 -0
- package/dist/tracking/pendingTransferMiddlewares/buildPendingTransferMiddlewares.test.js.map +1 -0
- package/dist/tracking/pendingTransferMiddlewares/matchBody.d.ts +12 -0
- package/dist/tracking/pendingTransferMiddlewares/matchBody.d.ts.map +1 -0
- package/dist/tracking/pendingTransferMiddlewares/matchBody.js +29 -0
- package/dist/tracking/pendingTransferMiddlewares/matchBody.js.map +1 -0
- package/dist/tracking/pendingTransferMiddlewares/relayerDenylistRules.d.ts +59 -0
- package/dist/tracking/pendingTransferMiddlewares/relayerDenylistRules.d.ts.map +1 -0
- package/dist/tracking/pendingTransferMiddlewares/relayerDenylistRules.js +661 -0
- package/dist/tracking/pendingTransferMiddlewares/relayerDenylistRules.js.map +1 -0
- package/dist/tracking/pendingTransferMiddlewares/relayerDenylistRules.test.d.ts +2 -0
- package/dist/tracking/pendingTransferMiddlewares/relayerDenylistRules.test.d.ts.map +1 -0
- package/dist/tracking/pendingTransferMiddlewares/relayerDenylistRules.test.js +1093 -0
- package/dist/tracking/pendingTransferMiddlewares/relayerDenylistRules.test.js.map +1 -0
- package/dist/tracking/pendingTransferMiddlewares/types.d.ts +2 -0
- package/dist/tracking/pendingTransferMiddlewares/types.d.ts.map +1 -0
- package/dist/tracking/pendingTransferMiddlewares/types.js +4 -0
- package/dist/tracking/pendingTransferMiddlewares/types.js.map +1 -0
- package/dist/tracking/store/InMemoryStore.d.ts.map +1 -1
- package/dist/utils/ExplorerClient.d.ts +1 -0
- package/dist/utils/ExplorerClient.d.ts.map +1 -1
- package/dist/utils/ExplorerClient.js +8 -3
- package/dist/utils/ExplorerClient.js.map +1 -1
- package/dist/utils/ExplorerClient.test.js +25 -0
- package/dist/utils/ExplorerClient.test.js.map +1 -1
- package/dist/utils/balanceUtils.d.ts +7 -1
- package/dist/utils/balanceUtils.d.ts.map +1 -1
- package/dist/utils/balanceUtils.js +39 -1
- package/dist/utils/balanceUtils.js.map +1 -1
- package/dist/utils/balanceUtils.test.js +55 -1
- package/dist/utils/balanceUtils.test.js.map +1 -1
- package/dist/utils/blockTag.d.ts +5 -4
- package/dist/utils/blockTag.d.ts.map +1 -1
- package/dist/utils/blockTag.js +26 -10
- package/dist/utils/blockTag.js.map +1 -1
- package/dist/utils/blockTag.test.d.ts +2 -0
- package/dist/utils/blockTag.test.d.ts.map +1 -0
- package/dist/utils/blockTag.test.js +132 -0
- package/dist/utils/blockTag.test.js.map +1 -0
- package/dist/utils/bridgeUtils.js.map +1 -1
- package/dist/utils/errors.d.ts.map +1 -1
- package/dist/utils/gasEstimation.d.ts +2 -1
- package/dist/utils/gasEstimation.d.ts.map +1 -1
- package/dist/utils/gasEstimation.js +16 -13
- package/dist/utils/gasEstimation.js.map +1 -1
- package/dist/utils/gasEstimation.test.d.ts +2 -0
- package/dist/utils/gasEstimation.test.d.ts.map +1 -0
- package/dist/utils/gasEstimation.test.js +104 -0
- package/dist/utils/gasEstimation.test.js.map +1 -0
- package/dist/utils/tokenUtils.d.ts.map +1 -1
- package/dist/utils/tokenUtils.js +5 -2
- package/dist/utils/tokenUtils.js.map +1 -1
- package/package.json +23 -22
- package/src/bridges/LiFiBridge.test.ts +270 -0
- package/src/bridges/LiFiBridge.ts +90 -71
- package/src/config/RebalancerConfig.test.ts +315 -1
- package/src/config/RebalancerConfig.ts +6 -3
- package/src/config/types.ts +98 -8
- package/src/core/InventoryRebalancer.test.ts +1092 -38
- package/src/core/InventoryRebalancer.ts +584 -351
- package/src/core/Rebalancer.test.ts +84 -0
- package/src/core/Rebalancer.ts +22 -6
- package/src/core/RebalancerOrchestrator.test.ts +0 -1
- package/src/core/RebalancerService.test.ts +35 -0
- package/src/core/RebalancerService.ts +3 -2
- package/src/e2e/harness/BaseLocalDeploymentManager.ts +3 -1
- package/src/e2e/harness/LocalProvider.test.ts +16 -0
- package/src/e2e/harness/LocalProvider.ts +10 -0
- package/src/e2e/harness/NativeLocalDeploymentManager.ts +2 -1
- package/src/e2e/harness/SnapshotHelper.ts +2 -5
- package/src/e2e/harness/TestHelpers.ts +3 -3
- package/src/e2e/harness/TestRebalancer.ts +21 -1
- package/src/e2e/relayer-denylist.e2e-test.ts +558 -0
- package/src/factories/RebalancerContextFactory.test.ts +489 -8
- package/src/factories/RebalancerContextFactory.ts +86 -36
- package/src/index.ts +2 -0
- package/src/interfaces/IRebalancer.ts +4 -1
- package/src/metrics/Metrics.ts +1 -0
- package/src/metrics/PriceGetter.test.ts +113 -0
- package/src/metrics/PriceGetter.ts +18 -3
- package/src/monitor/Monitor.test.ts +408 -0
- package/src/monitor/Monitor.ts +123 -97
- package/src/service.ts +14 -8
- package/src/strategy/BaseStrategy.ts +18 -15
- package/src/strategy/CollateralDeficitStrategy.ts +4 -3
- package/src/strategy/MinAmountStrategy.test.ts +238 -2
- package/src/strategy/MinAmountStrategy.ts +29 -17
- package/src/test/helpers.ts +13 -12
- package/src/test/lifiMocks.ts +5 -2
- package/src/test/pendingTransferMiddlewareMocks.ts +65 -0
- package/src/tracking/ActionTracker.test.ts +635 -3
- package/src/tracking/ActionTracker.ts +92 -12
- package/src/tracking/IActionTracker.ts +4 -2
- package/src/tracking/pendingTransferMiddlewares/BlacklistIsmMiddleware.test.ts +1693 -0
- package/src/tracking/pendingTransferMiddlewares/BlacklistIsmMiddleware.ts +784 -0
- package/src/tracking/pendingTransferMiddlewares/PendingTransferMiddleware.test.ts +554 -0
- package/src/tracking/pendingTransferMiddlewares/PendingTransferMiddleware.ts +233 -0
- package/src/tracking/pendingTransferMiddlewares/RelayerDenylistMiddleware.test.ts +894 -0
- package/src/tracking/pendingTransferMiddlewares/RelayerDenylistMiddleware.ts +404 -0
- package/src/tracking/pendingTransferMiddlewares/buildPendingTransferMiddlewares.test.ts +205 -0
- package/src/tracking/pendingTransferMiddlewares/buildPendingTransferMiddlewares.ts +89 -0
- package/src/tracking/pendingTransferMiddlewares/fixtures/mainnet-denylist.json +3556 -0
- package/src/tracking/pendingTransferMiddlewares/matchBody.ts +43 -0
- package/src/tracking/pendingTransferMiddlewares/relayerDenylistRules.test.ts +1220 -0
- package/src/tracking/pendingTransferMiddlewares/relayerDenylistRules.ts +934 -0
- package/src/tracking/pendingTransferMiddlewares/types.ts +3 -0
- package/src/utils/ExplorerClient.test.ts +42 -0
- package/src/utils/ExplorerClient.ts +14 -3
- package/src/utils/balanceUtils.test.ts +87 -1
- package/src/utils/balanceUtils.ts +73 -2
- package/src/utils/blockTag.test.ts +173 -0
- package/src/utils/blockTag.ts +35 -15
- package/src/utils/gasEstimation.test.ts +192 -0
- package/src/utils/gasEstimation.ts +17 -11
- package/src/utils/tokenUtils.ts +5 -2
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
import { open } from 'node:fs/promises';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import type { Logger } from 'pino';
|
|
4
|
+
|
|
5
|
+
import { assert, sleep, timeout } from '@humanode-hyperlane/utils';
|
|
6
|
+
|
|
7
|
+
import { PendingTransferMiddlewareType } from '../../config/types.js';
|
|
8
|
+
import type {
|
|
9
|
+
ExplorerMessage,
|
|
10
|
+
IExplorerClient,
|
|
11
|
+
RebalanceActionQueryParams,
|
|
12
|
+
} from '../../utils/ExplorerClient.js';
|
|
13
|
+
|
|
14
|
+
import { PendingTransferMiddleware } from './PendingTransferMiddleware.js';
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
DenylistLoadError,
|
|
18
|
+
DenylistLoadFailure,
|
|
19
|
+
type DenylistRules,
|
|
20
|
+
findDenyReason,
|
|
21
|
+
findMessageIdDenyReason,
|
|
22
|
+
parseDenylistFile,
|
|
23
|
+
} from './relayerDenylistRules.js';
|
|
24
|
+
import { normalizeMessageId } from './types.js';
|
|
25
|
+
|
|
26
|
+
export const FETCH_TIMEOUT_MS = 10_000;
|
|
27
|
+
|
|
28
|
+
/** Startup fetch attempts and the delay between them, as in the relayer. */
|
|
29
|
+
const INITIAL_LOAD_ATTEMPTS = 3;
|
|
30
|
+
export const INITIAL_LOAD_RETRY_DELAY_MS = 2_000;
|
|
31
|
+
|
|
32
|
+
/** The relayer's limit on a denylist file, in bytes. */
|
|
33
|
+
export const MAX_DENYLIST_FILE_BYTES = 4 * 1024 * 1024;
|
|
34
|
+
|
|
35
|
+
export const RELAYER_DENYLIST_REFRESH_FAILED_MESSAGE =
|
|
36
|
+
'Relayer denylist refresh failed, keeping last good rules';
|
|
37
|
+
|
|
38
|
+
export const RELAYER_DENYLIST_INITIAL_LOAD_RETRY_MESSAGE =
|
|
39
|
+
'Failed to fetch relayer denylist at startup, retrying';
|
|
40
|
+
|
|
41
|
+
export const RELAYER_DENYLIST_CHECK_FAILED_MESSAGE =
|
|
42
|
+
'Failed to check pending transfer against relayer denylist, keeping it';
|
|
43
|
+
|
|
44
|
+
export interface RelayerDenylistMiddlewareOptions {
|
|
45
|
+
url: string;
|
|
46
|
+
refreshIntervalMs: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface DenylistSource {
|
|
50
|
+
url: URL;
|
|
51
|
+
redactedUrl: string;
|
|
52
|
+
refreshIntervalMs: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Drops credentials, query and fragment, which may carry a token. */
|
|
56
|
+
export function redactUrl(url: string): string {
|
|
57
|
+
const parsed = new URL(url);
|
|
58
|
+
|
|
59
|
+
return parsed.protocol === 'file:'
|
|
60
|
+
? `file://${parsed.pathname}`
|
|
61
|
+
: `${parsed.protocol}//${parsed.host}${parsed.pathname}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Removes the URL and its secret-bearing parts from an error message. */
|
|
65
|
+
function scrubUrl(message: string, url: URL): string {
|
|
66
|
+
let scrubbed = message.split(url.href).join(redactUrl(url.href));
|
|
67
|
+
for (const secret of [url.username, url.password, url.search]) {
|
|
68
|
+
if (secret) scrubbed = scrubbed.split(secret).join('[redacted]');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return scrubbed;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function fileTooLarge(bytes: number): Error {
|
|
75
|
+
return new Error(
|
|
76
|
+
`Denylist file is at least ${bytes} bytes; the limit is ${MAX_DENYLIST_FILE_BYTES}`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Decodes like the relayer's serde_json parser: invalid UTF-8 is an invalid
|
|
82
|
+
* file, and a leading BOM is kept so `JSON.parse` rejects it.
|
|
83
|
+
*/
|
|
84
|
+
function decodeUtf8(bytes: Uint8Array): string {
|
|
85
|
+
try {
|
|
86
|
+
return new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(
|
|
87
|
+
bytes,
|
|
88
|
+
);
|
|
89
|
+
} catch (error: unknown) {
|
|
90
|
+
if (!(error instanceof TypeError)) throw error;
|
|
91
|
+
throw new DenylistLoadError(
|
|
92
|
+
DenylistLoadFailure.InvalidFile,
|
|
93
|
+
'Invalid denylist JSON: not valid UTF-8',
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Reads at most one byte past the relayer's limit, rejecting a larger file. */
|
|
99
|
+
async function readFileCapped(path: string): Promise<Uint8Array> {
|
|
100
|
+
const file = await open(path);
|
|
101
|
+
try {
|
|
102
|
+
const buffer = Buffer.alloc(MAX_DENYLIST_FILE_BYTES + 1);
|
|
103
|
+
let bytes = 0;
|
|
104
|
+
while (bytes < buffer.length) {
|
|
105
|
+
const { bytesRead } = await file.read(
|
|
106
|
+
buffer,
|
|
107
|
+
bytes,
|
|
108
|
+
buffer.length - bytes,
|
|
109
|
+
);
|
|
110
|
+
if (bytesRead === 0) break;
|
|
111
|
+
bytes += bytesRead;
|
|
112
|
+
}
|
|
113
|
+
if (bytes > MAX_DENYLIST_FILE_BYTES) throw fileTooLarge(bytes);
|
|
114
|
+
|
|
115
|
+
return buffer.subarray(0, bytes);
|
|
116
|
+
} finally {
|
|
117
|
+
await file.close();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Reads at most the relayer's limit, rejecting a larger body before buffering
|
|
123
|
+
* it. Cancels the body once `signal` aborts, as a fetch wrapper may not.
|
|
124
|
+
*/
|
|
125
|
+
async function readBodyCapped(
|
|
126
|
+
response: Response,
|
|
127
|
+
signal: AbortSignal,
|
|
128
|
+
): Promise<Uint8Array> {
|
|
129
|
+
const length = Number(response.headers.get('content-length'));
|
|
130
|
+
if (length > MAX_DENYLIST_FILE_BYTES) throw fileTooLarge(length);
|
|
131
|
+
|
|
132
|
+
const chunks: Uint8Array[] = [];
|
|
133
|
+
let bytes = 0;
|
|
134
|
+
const reader = response.body?.getReader();
|
|
135
|
+
// A rejected cancel means the stream already errored, which ends the read too.
|
|
136
|
+
const cancel = (): void => {
|
|
137
|
+
void reader?.cancel().catch(() => {});
|
|
138
|
+
};
|
|
139
|
+
if (signal.aborted) cancel();
|
|
140
|
+
else signal.addEventListener('abort', cancel, { once: true });
|
|
141
|
+
while (reader) {
|
|
142
|
+
const { done, value } = await reader.read();
|
|
143
|
+
if (done) break;
|
|
144
|
+
|
|
145
|
+
bytes += value.byteLength;
|
|
146
|
+
if (bytes > MAX_DENYLIST_FILE_BYTES) {
|
|
147
|
+
await reader.cancel();
|
|
148
|
+
throw fileTooLarge(bytes);
|
|
149
|
+
}
|
|
150
|
+
chunks.push(value);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return Buffer.concat(chunks);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function fetchBodyCapped(
|
|
157
|
+
url: URL,
|
|
158
|
+
signal: AbortSignal,
|
|
159
|
+
): Promise<Uint8Array> {
|
|
160
|
+
const response = await fetch(url, { signal });
|
|
161
|
+
if (!response.ok) {
|
|
162
|
+
throw new Error(`HTTP status ${response.status}`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return readBodyCapped(response, signal);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function readDenylistBytes(url: URL): Promise<Uint8Array> {
|
|
169
|
+
switch (url.protocol) {
|
|
170
|
+
case 'file:':
|
|
171
|
+
return readFileCapped(fileURLToPath(url));
|
|
172
|
+
case 'http:':
|
|
173
|
+
case 'https:': {
|
|
174
|
+
// The timer settles the load even if the fetch ignores the abort.
|
|
175
|
+
const controller = new AbortController();
|
|
176
|
+
try {
|
|
177
|
+
return await timeout(
|
|
178
|
+
fetchBodyCapped(url, controller.signal),
|
|
179
|
+
FETCH_TIMEOUT_MS,
|
|
180
|
+
`Denylist fetch timed out after ${FETCH_TIMEOUT_MS} ms`,
|
|
181
|
+
);
|
|
182
|
+
} finally {
|
|
183
|
+
controller.abort();
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
default:
|
|
187
|
+
throw new Error(`Unsupported denylist URL protocol ${url.protocol}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export async function fetchDenylistFile(url: URL): Promise<string> {
|
|
192
|
+
let bytes: Uint8Array;
|
|
193
|
+
try {
|
|
194
|
+
bytes = await readDenylistBytes(url);
|
|
195
|
+
} catch (error: unknown) {
|
|
196
|
+
throw new DenylistLoadError(
|
|
197
|
+
DenylistLoadFailure.FetchFailed,
|
|
198
|
+
scrubUrl(error instanceof Error ? error.message : String(error), url),
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return decodeUtf8(bytes);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function loadRules(
|
|
206
|
+
source: DenylistSource,
|
|
207
|
+
logger: Logger,
|
|
208
|
+
): Promise<DenylistRules> {
|
|
209
|
+
const rules = parseDenylistFile(await fetchDenylistFile(source.url));
|
|
210
|
+
|
|
211
|
+
logger.debug(
|
|
212
|
+
{
|
|
213
|
+
url: source.redactedUrl,
|
|
214
|
+
matchRules: rules.matchRules.length,
|
|
215
|
+
addressRules: rules.addressRules.length,
|
|
216
|
+
},
|
|
217
|
+
'Loaded relayer denylist',
|
|
218
|
+
);
|
|
219
|
+
return rules;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function loadFailure(error: unknown): DenylistLoadFailure {
|
|
223
|
+
return error instanceof DenylistLoadError
|
|
224
|
+
? error.failure
|
|
225
|
+
: DenylistLoadFailure.FetchFailed;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Retries fetch failures only: an invalid file stays invalid. */
|
|
229
|
+
async function loadInitialRules(
|
|
230
|
+
source: DenylistSource,
|
|
231
|
+
logger: Logger,
|
|
232
|
+
): Promise<DenylistRules> {
|
|
233
|
+
for (let attempt = 1; ; attempt++) {
|
|
234
|
+
try {
|
|
235
|
+
return await loadRules(source, logger);
|
|
236
|
+
} catch (error: unknown) {
|
|
237
|
+
if (
|
|
238
|
+
attempt >= INITIAL_LOAD_ATTEMPTS ||
|
|
239
|
+
loadFailure(error) !== DenylistLoadFailure.FetchFailed
|
|
240
|
+
) {
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
logger.warn(
|
|
245
|
+
{
|
|
246
|
+
url: source.redactedUrl,
|
|
247
|
+
attempt,
|
|
248
|
+
error: error instanceof Error ? error.message : String(error),
|
|
249
|
+
},
|
|
250
|
+
RELAYER_DENYLIST_INITIAL_LOAD_RETRY_MESSAGE,
|
|
251
|
+
);
|
|
252
|
+
await sleep(INITIAL_LOAD_RETRY_DELAY_MS);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Ignores messages the relayer's runtime denylist blocks. `create()` loads the
|
|
259
|
+
* file; it is re-fetched once `refreshIntervalMs` has passed, and a failed
|
|
260
|
+
* refresh keeps the last good rules.
|
|
261
|
+
*
|
|
262
|
+
* `address` rules are checked against the Explorer's `origin_tx_sender`, the
|
|
263
|
+
* origin transaction's fee payer, where the relayer checks every origin
|
|
264
|
+
* transaction signer. A denied depositor whose transaction another account
|
|
265
|
+
* paid for (e.g. a sponsored Sealevel transfer) is therefore not ignored and
|
|
266
|
+
* stays reserved.
|
|
267
|
+
*
|
|
268
|
+
* The relayer also skips its signer check, so it may deliver a message this
|
|
269
|
+
* middleware ignores for its `origin_tx_sender` alone, when no dispatch tx
|
|
270
|
+
* hash is stored (Cosmos block-level events, an old DB backlog, a zero hash),
|
|
271
|
+
* the origin tx is not found (pruned or reorged out), the sender is zero
|
|
272
|
+
* (Radix, Aleo) or the lookup keeps failing for about 8 minutes
|
|
273
|
+
* (rust/main/agents/relayer/src/msg/pending_message.rs and
|
|
274
|
+
* runtime_denylist.rs).
|
|
275
|
+
*
|
|
276
|
+
* Only `match` rules that set nothing but `messageId` apply to the
|
|
277
|
+
* rebalancer's own rebalance messages, which then stop counting as incoming
|
|
278
|
+
* collateral. Deny a rebalance message only once its funds settled another
|
|
279
|
+
* way; otherwise the rebalancer sends a replacement.
|
|
280
|
+
*/
|
|
281
|
+
export class RelayerDenylistMiddleware extends PendingTransferMiddleware {
|
|
282
|
+
private lastAttemptAt: number;
|
|
283
|
+
private refreshing?: Promise<void>;
|
|
284
|
+
|
|
285
|
+
private constructor(
|
|
286
|
+
inner: IExplorerClient,
|
|
287
|
+
private readonly source: DenylistSource,
|
|
288
|
+
private rules: DenylistRules,
|
|
289
|
+
private loadedAt: number,
|
|
290
|
+
) {
|
|
291
|
+
super(inner, PendingTransferMiddlewareType.RelayerDenylist);
|
|
292
|
+
|
|
293
|
+
this.lastAttemptAt = loadedAt;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Loads the denylist, retrying fetch failures. Throws if it still cannot be
|
|
298
|
+
* fetched or is invalid.
|
|
299
|
+
*/
|
|
300
|
+
static async create(
|
|
301
|
+
inner: IExplorerClient,
|
|
302
|
+
options: RelayerDenylistMiddlewareOptions,
|
|
303
|
+
logger: Logger,
|
|
304
|
+
): Promise<RelayerDenylistMiddleware> {
|
|
305
|
+
const url = new URL(options.url);
|
|
306
|
+
const redactedUrl = redactUrl(options.url);
|
|
307
|
+
assert(
|
|
308
|
+
url.username === '' && url.password === '',
|
|
309
|
+
`Relayer denylist url ${redactedUrl} must not contain credentials`,
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
const source: DenylistSource = {
|
|
313
|
+
url,
|
|
314
|
+
redactedUrl,
|
|
315
|
+
refreshIntervalMs: options.refreshIntervalMs,
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
let rules: DenylistRules;
|
|
319
|
+
try {
|
|
320
|
+
rules = await loadInitialRules(source, logger);
|
|
321
|
+
} catch (error: unknown) {
|
|
322
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
323
|
+
throw new Error(
|
|
324
|
+
`Failed to load relayer denylist from ${redactedUrl} (${loadFailure(error)}): ${message}`,
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return new RelayerDenylistMiddleware(inner, source, rules, Date.now());
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
protected async findIgnored(
|
|
332
|
+
messages: ExplorerMessage[],
|
|
333
|
+
logger: Logger,
|
|
334
|
+
): Promise<Map<string, string>> {
|
|
335
|
+
await this.refreshIfStale(logger);
|
|
336
|
+
|
|
337
|
+
const now = Date.now();
|
|
338
|
+
const ignored = new Map<string, string>();
|
|
339
|
+
for (const msg of messages) {
|
|
340
|
+
try {
|
|
341
|
+
const reason = findDenyReason(this.rules, msg, now);
|
|
342
|
+
if (reason !== undefined) {
|
|
343
|
+
ignored.set(normalizeMessageId(msg.msg_id), reason);
|
|
344
|
+
}
|
|
345
|
+
} catch (error: unknown) {
|
|
346
|
+
logger.warn(
|
|
347
|
+
{
|
|
348
|
+
msgId: msg.msg_id,
|
|
349
|
+
error: error instanceof Error ? error.message : String(error),
|
|
350
|
+
},
|
|
351
|
+
RELAYER_DENYLIST_CHECK_FAILED_MESSAGE,
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return ignored;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
override isIgnoredRebalanceAction(messageId: string): boolean {
|
|
360
|
+
return (
|
|
361
|
+
findMessageIdDenyReason(this.rules, messageId, Date.now()) !==
|
|
362
|
+
undefined || super.isIgnoredRebalanceAction(messageId)
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Keeps the rules `isIgnoredRebalanceAction` reads fresh; rows pass through.
|
|
367
|
+
override async getInflightRebalanceActions(
|
|
368
|
+
params: RebalanceActionQueryParams,
|
|
369
|
+
logger: Logger,
|
|
370
|
+
): Promise<ExplorerMessage[]> {
|
|
371
|
+
await this.refreshIfStale(logger);
|
|
372
|
+
return super.getInflightRebalanceActions(params, logger);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
private refreshIfStale(logger: Logger): Promise<void> {
|
|
376
|
+
if (this.refreshing) return this.refreshing;
|
|
377
|
+
if (Date.now() - this.lastAttemptAt < this.source.refreshIntervalMs) {
|
|
378
|
+
return Promise.resolve();
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
this.lastAttemptAt = Date.now();
|
|
382
|
+
this.refreshing = this.reload(logger).finally(() => {
|
|
383
|
+
this.refreshing = undefined;
|
|
384
|
+
});
|
|
385
|
+
return this.refreshing;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
private async reload(logger: Logger): Promise<void> {
|
|
389
|
+
try {
|
|
390
|
+
this.rules = await loadRules(this.source, logger);
|
|
391
|
+
this.loadedAt = Date.now();
|
|
392
|
+
} catch (error: unknown) {
|
|
393
|
+
logger.warn(
|
|
394
|
+
{
|
|
395
|
+
url: this.source.redactedUrl,
|
|
396
|
+
failure: loadFailure(error),
|
|
397
|
+
cachedRulesAgeSecs: Math.floor((Date.now() - this.loadedAt) / 1000),
|
|
398
|
+
error: error instanceof Error ? error.message : String(error),
|
|
399
|
+
},
|
|
400
|
+
RELAYER_DENYLIST_REFRESH_FAILED_MESSAGE,
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { expect } from 'chai';
|
|
2
|
+
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { pathToFileURL } from 'node:url';
|
|
6
|
+
import { type Logger, pino } from 'pino';
|
|
7
|
+
import Sinon from 'sinon';
|
|
8
|
+
|
|
9
|
+
import { MultiProvider } from '@humanode-hyperlane/sdk';
|
|
10
|
+
import { ProtocolType, assert } from '@humanode-hyperlane/utils';
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
type PendingTransferMiddlewareConfig,
|
|
14
|
+
PendingTransferMiddlewareType,
|
|
15
|
+
} from '../../config/types.js';
|
|
16
|
+
import {
|
|
17
|
+
ID_A,
|
|
18
|
+
ID_B,
|
|
19
|
+
ID_C,
|
|
20
|
+
PARAMS,
|
|
21
|
+
REBALANCE_PARAMS,
|
|
22
|
+
StubExplorerClient,
|
|
23
|
+
buildMessage,
|
|
24
|
+
} from '../../test/pendingTransferMiddlewareMocks.js';
|
|
25
|
+
|
|
26
|
+
import { INITIAL_LOAD_RETRY_DELAY_MS } from './RelayerDenylistMiddleware.js';
|
|
27
|
+
import {
|
|
28
|
+
type PendingTransferMiddlewareDeps,
|
|
29
|
+
buildPendingTransferMiddlewares,
|
|
30
|
+
} from './buildPendingTransferMiddlewares.js';
|
|
31
|
+
|
|
32
|
+
describe('buildPendingTransferMiddlewares', () => {
|
|
33
|
+
const logger = pino({ level: 'silent' });
|
|
34
|
+
let deps: PendingTransferMiddlewareDeps;
|
|
35
|
+
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
const multiProvider = Sinon.createStubInstance(MultiProvider);
|
|
38
|
+
multiProvider.getProtocol.returns(ProtocolType.Ethereum);
|
|
39
|
+
multiProvider.getProvider.throws(new Error('No provider in this test'));
|
|
40
|
+
|
|
41
|
+
deps = {
|
|
42
|
+
routersByDomain: { 1: '0x67899664446ccfadf75e0d3f5668be262acd1069' },
|
|
43
|
+
mailboxByDomain: { 1: '0xc005dc82818d67af737725bd4bf75435d065d239' },
|
|
44
|
+
multiProvider,
|
|
45
|
+
logger,
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('returns the inner client unchanged when no middlewares are configured', async () => {
|
|
50
|
+
const inner = new StubExplorerClient([]);
|
|
51
|
+
|
|
52
|
+
const result = await buildPendingTransferMiddlewares(inner, [], deps);
|
|
53
|
+
|
|
54
|
+
expect(result.client).to.equal(inner);
|
|
55
|
+
expect(result.isIgnoredTransfer).to.equal(undefined);
|
|
56
|
+
expect(result.isIgnoredRebalanceAction).to.equal(undefined);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('builds layers in config order and unions their ignored ids', async () => {
|
|
60
|
+
const dir = mkdtempSync(join(tmpdir(), 'pending-transfer-middlewares-'));
|
|
61
|
+
try {
|
|
62
|
+
const writeDenylist = (name: string, ids: string[]): string => {
|
|
63
|
+
const path = join(dir, `${name}.json`);
|
|
64
|
+
writeFileSync(
|
|
65
|
+
path,
|
|
66
|
+
JSON.stringify({
|
|
67
|
+
rules: ids.map((messageId) => ({
|
|
68
|
+
reason: name,
|
|
69
|
+
addedBy: 'tester',
|
|
70
|
+
match: { messageId },
|
|
71
|
+
})),
|
|
72
|
+
}),
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
return pathToFileURL(path).href;
|
|
76
|
+
};
|
|
77
|
+
const configs: PendingTransferMiddlewareConfig[] = [
|
|
78
|
+
{
|
|
79
|
+
type: PendingTransferMiddlewareType.RelayerDenylist,
|
|
80
|
+
url: writeDenylist('first', [ID_A, ID_B]),
|
|
81
|
+
refreshIntervalSecs: 60,
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
type: PendingTransferMiddlewareType.RelayerDenylist,
|
|
85
|
+
url: writeDenylist('second', [ID_A]),
|
|
86
|
+
refreshIntervalSecs: 60,
|
|
87
|
+
},
|
|
88
|
+
];
|
|
89
|
+
const inner = new StubExplorerClient([
|
|
90
|
+
buildMessage(ID_A),
|
|
91
|
+
buildMessage(ID_B),
|
|
92
|
+
buildMessage(ID_C),
|
|
93
|
+
]);
|
|
94
|
+
const queryLogger: Logger = pino({ level: 'silent' });
|
|
95
|
+
const info = Sinon.spy(queryLogger, 'info');
|
|
96
|
+
|
|
97
|
+
inner.rebalanceActions = [buildMessage(ID_B), buildMessage(ID_C)];
|
|
98
|
+
|
|
99
|
+
const { client, isIgnoredTransfer, isIgnoredRebalanceAction } =
|
|
100
|
+
await buildPendingTransferMiddlewares(inner, configs, deps);
|
|
101
|
+
const result = await client.getInflightUserTransfers(PARAMS, queryLogger);
|
|
102
|
+
const rebalanceActions = await client.getInflightRebalanceActions(
|
|
103
|
+
REBALANCE_PARAMS,
|
|
104
|
+
queryLogger,
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
expect(result.map((msg) => msg.msg_id)).to.deep.equal([ID_C]);
|
|
108
|
+
|
|
109
|
+
// Item 0 is innermost, so it sees ID_A first and records its reason.
|
|
110
|
+
const ignoredLogs = info
|
|
111
|
+
.getCalls()
|
|
112
|
+
.filter((call) => call.args.includes('Ignoring pending transfer'));
|
|
113
|
+
expect(ignoredLogs).to.have.lengthOf(2);
|
|
114
|
+
for (const id of [ID_A, ID_B]) {
|
|
115
|
+
expect(
|
|
116
|
+
info.calledWith(
|
|
117
|
+
Sinon.match({ msgId: id, reason: 'first (added by tester)' }),
|
|
118
|
+
'Ignoring pending transfer',
|
|
119
|
+
),
|
|
120
|
+
).to.be.true;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
assert(isIgnoredTransfer, 'Expected an ignored-transfer predicate');
|
|
124
|
+
expect(isIgnoredTransfer(ID_A)).to.be.true;
|
|
125
|
+
expect(isIgnoredTransfer(ID_B)).to.be.true;
|
|
126
|
+
expect(isIgnoredTransfer(ID_C)).to.be.false;
|
|
127
|
+
|
|
128
|
+
expect(rebalanceActions.map((msg) => msg.msg_id)).to.deep.equal([
|
|
129
|
+
ID_B,
|
|
130
|
+
ID_C,
|
|
131
|
+
]);
|
|
132
|
+
assert(
|
|
133
|
+
isIgnoredRebalanceAction,
|
|
134
|
+
'Expected an ignored-rebalance-action predicate',
|
|
135
|
+
);
|
|
136
|
+
expect(isIgnoredRebalanceAction(ID_B)).to.be.true;
|
|
137
|
+
expect(isIgnoredRebalanceAction(ID_C)).to.be.false;
|
|
138
|
+
} finally {
|
|
139
|
+
rmSync(dir, { recursive: true, force: true });
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('fails when the BlacklistIsm trees cannot be resolved at startup', async () => {
|
|
144
|
+
const inner = new StubExplorerClient([]);
|
|
145
|
+
const configs: PendingTransferMiddlewareConfig[] = [
|
|
146
|
+
{
|
|
147
|
+
type: PendingTransferMiddlewareType.BlacklistIsm,
|
|
148
|
+
refreshIntervalSecs: 600,
|
|
149
|
+
},
|
|
150
|
+
];
|
|
151
|
+
|
|
152
|
+
const error = await buildPendingTransferMiddlewares(
|
|
153
|
+
inner,
|
|
154
|
+
configs,
|
|
155
|
+
deps,
|
|
156
|
+
).then(
|
|
157
|
+
() => expect.fail('Expected buildPendingTransferMiddlewares to throw'),
|
|
158
|
+
(err: unknown) => err,
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
expect(error).to.be.instanceOf(Error);
|
|
162
|
+
if (error instanceof Error) {
|
|
163
|
+
expect(error.message).to.equal(
|
|
164
|
+
'Failed to resolve the ISM of router 0x67899664446ccfadf75e0d3f5668be262acd1069 on domain 1: No provider in this test',
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('fails when a relayer denylist cannot be loaded at startup', async () => {
|
|
170
|
+
const inner = new StubExplorerClient([]);
|
|
171
|
+
const configs: PendingTransferMiddlewareConfig[] = [
|
|
172
|
+
{
|
|
173
|
+
type: PendingTransferMiddlewareType.RelayerDenylist,
|
|
174
|
+
url: 'https://example.com/denylist.json',
|
|
175
|
+
refreshIntervalSecs: 60,
|
|
176
|
+
},
|
|
177
|
+
];
|
|
178
|
+
// Startup retries a failed fetch after a delay; a stubbed fetch keeps
|
|
179
|
+
// the faked delays off real I/O.
|
|
180
|
+
const clock = Sinon.useFakeTimers({
|
|
181
|
+
toFake: ['setTimeout', 'clearTimeout'],
|
|
182
|
+
});
|
|
183
|
+
const fetchStub = Sinon.stub(globalThis, 'fetch').rejects(
|
|
184
|
+
new Error('getaddrinfo ENOTFOUND example.com'),
|
|
185
|
+
);
|
|
186
|
+
try {
|
|
187
|
+
const result = buildPendingTransferMiddlewares(inner, configs, deps).then(
|
|
188
|
+
() => expect.fail('Expected buildPendingTransferMiddlewares to throw'),
|
|
189
|
+
(err: unknown) => err,
|
|
190
|
+
);
|
|
191
|
+
await clock.tickAsync(2 * INITIAL_LOAD_RETRY_DELAY_MS);
|
|
192
|
+
const error = await result;
|
|
193
|
+
|
|
194
|
+
expect(error).to.be.instanceOf(Error);
|
|
195
|
+
if (error instanceof Error) {
|
|
196
|
+
expect(error.message).to.equal(
|
|
197
|
+
'Failed to load relayer denylist from https://example.com/denylist.json (fetch_failed): getaddrinfo ENOTFOUND example.com',
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
} finally {
|
|
201
|
+
fetchStub.restore();
|
|
202
|
+
clock.restore();
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { Logger } from 'pino';
|
|
2
|
+
|
|
3
|
+
import type { MultiProvider } from '@humanode-hyperlane/sdk';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
type PendingTransferMiddlewareConfig,
|
|
7
|
+
PendingTransferMiddlewareType,
|
|
8
|
+
} from '../../config/types.js';
|
|
9
|
+
import type { IExplorerClient } from '../../utils/ExplorerClient.js';
|
|
10
|
+
|
|
11
|
+
import { BlacklistIsmMiddleware } from './BlacklistIsmMiddleware.js';
|
|
12
|
+
import type { PendingTransferMiddleware } from './PendingTransferMiddleware.js';
|
|
13
|
+
import { RelayerDenylistMiddleware } from './RelayerDenylistMiddleware.js';
|
|
14
|
+
|
|
15
|
+
export interface PendingTransferMiddlewareDeps {
|
|
16
|
+
routersByDomain: Record<number, string>;
|
|
17
|
+
mailboxByDomain: Record<number, string>;
|
|
18
|
+
multiProvider: MultiProvider;
|
|
19
|
+
logger: Logger;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface PendingTransferMiddlewares {
|
|
23
|
+
client: IExplorerClient;
|
|
24
|
+
/** Set only when at least one middleware is configured. */
|
|
25
|
+
isIgnoredTransfer?: (messageId: string) => boolean;
|
|
26
|
+
/** Set only when at least one middleware is configured. */
|
|
27
|
+
isIgnoredRebalanceAction?: (messageId: string) => boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function createMiddleware(
|
|
31
|
+
config: PendingTransferMiddlewareConfig,
|
|
32
|
+
inner: IExplorerClient,
|
|
33
|
+
deps: PendingTransferMiddlewareDeps,
|
|
34
|
+
): Promise<PendingTransferMiddleware> {
|
|
35
|
+
switch (config.type) {
|
|
36
|
+
case PendingTransferMiddlewareType.RelayerDenylist:
|
|
37
|
+
return RelayerDenylistMiddleware.create(
|
|
38
|
+
inner,
|
|
39
|
+
{
|
|
40
|
+
url: config.url,
|
|
41
|
+
refreshIntervalMs: config.refreshIntervalSecs * 1_000,
|
|
42
|
+
},
|
|
43
|
+
deps.logger,
|
|
44
|
+
);
|
|
45
|
+
case PendingTransferMiddlewareType.BlacklistIsm:
|
|
46
|
+
return BlacklistIsmMiddleware.create(
|
|
47
|
+
inner,
|
|
48
|
+
{
|
|
49
|
+
refreshIntervalMs: config.refreshIntervalSecs * 1_000,
|
|
50
|
+
routersByDomain: deps.routersByDomain,
|
|
51
|
+
mailboxByDomain: deps.mailboxByDomain,
|
|
52
|
+
multiProvider: deps.multiProvider,
|
|
53
|
+
},
|
|
54
|
+
deps.logger,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Wraps `inner` with one middleware per configured item. Item 0 is the
|
|
61
|
+
* innermost middleware, so it sees the raw Explorer result first and each
|
|
62
|
+
* later item sees only what the earlier ones kept. With no items, `inner` is
|
|
63
|
+
* returned unchanged.
|
|
64
|
+
*/
|
|
65
|
+
export async function buildPendingTransferMiddlewares(
|
|
66
|
+
inner: IExplorerClient,
|
|
67
|
+
configs: PendingTransferMiddlewareConfig[],
|
|
68
|
+
deps: PendingTransferMiddlewareDeps,
|
|
69
|
+
): Promise<PendingTransferMiddlewares> {
|
|
70
|
+
let outermost: PendingTransferMiddleware | undefined;
|
|
71
|
+
for (const config of configs) {
|
|
72
|
+
outermost = await createMiddleware(config, outermost ?? inner, deps);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!outermost) return { client: inner };
|
|
76
|
+
|
|
77
|
+
deps.logger.info(
|
|
78
|
+
{ middlewares: configs.map((config) => config.type) },
|
|
79
|
+
'Pending transfer middlewares enabled',
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const client = outermost;
|
|
83
|
+
return {
|
|
84
|
+
client,
|
|
85
|
+
isIgnoredTransfer: (messageId) => client.isIgnored(messageId),
|
|
86
|
+
isIgnoredRebalanceAction: (messageId) =>
|
|
87
|
+
client.isIgnoredRebalanceAction(messageId),
|
|
88
|
+
};
|
|
89
|
+
}
|