@humanode-hyperlane/rebalancer 27.2.2 → 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,894 @@
|
|
|
1
|
+
import chai, { expect } from 'chai';
|
|
2
|
+
import chaiAsPromised from 'chai-as-promised';
|
|
3
|
+
import {
|
|
4
|
+
mkdtempSync,
|
|
5
|
+
renameSync,
|
|
6
|
+
rmSync,
|
|
7
|
+
truncateSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from 'node:fs';
|
|
10
|
+
import { tmpdir } from 'node:os';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import { pathToFileURL } from 'node:url';
|
|
13
|
+
import { type Logger, pino } from 'pino';
|
|
14
|
+
import Sinon from 'sinon';
|
|
15
|
+
|
|
16
|
+
import { assert } from '@humanode-hyperlane/utils';
|
|
17
|
+
|
|
18
|
+
import type {
|
|
19
|
+
ExplorerMessage,
|
|
20
|
+
IExplorerClient,
|
|
21
|
+
UserTransferQueryParams,
|
|
22
|
+
} from '../../utils/ExplorerClient.js';
|
|
23
|
+
import { REBALANCE_PARAMS } from '../../test/pendingTransferMiddlewareMocks.js';
|
|
24
|
+
|
|
25
|
+
import { PENDING_TRANSFER_MIDDLEWARE_FAILED_MESSAGE } from './PendingTransferMiddleware.js';
|
|
26
|
+
import {
|
|
27
|
+
FETCH_TIMEOUT_MS,
|
|
28
|
+
INITIAL_LOAD_RETRY_DELAY_MS,
|
|
29
|
+
MAX_DENYLIST_FILE_BYTES,
|
|
30
|
+
RELAYER_DENYLIST_CHECK_FAILED_MESSAGE,
|
|
31
|
+
RELAYER_DENYLIST_INITIAL_LOAD_RETRY_MESSAGE,
|
|
32
|
+
RELAYER_DENYLIST_REFRESH_FAILED_MESSAGE,
|
|
33
|
+
RelayerDenylistMiddleware,
|
|
34
|
+
fetchDenylistFile,
|
|
35
|
+
redactUrl,
|
|
36
|
+
} from './RelayerDenylistMiddleware.js';
|
|
37
|
+
import {
|
|
38
|
+
DenylistLoadError,
|
|
39
|
+
DenylistLoadFailure,
|
|
40
|
+
} from './relayerDenylistRules.js';
|
|
41
|
+
|
|
42
|
+
chai.use(chaiAsPromised);
|
|
43
|
+
|
|
44
|
+
const MESSAGE_ID =
|
|
45
|
+
'0x8960c5e41eb0d91973140ebc196892f5f07f512dd86047e8536e71424462c5d3';
|
|
46
|
+
const OTHER_MESSAGE_ID =
|
|
47
|
+
'0xdae65a8dd678bf8ea03faed7d53b99e52f5bdbae7c95834eff009cc37a18103c';
|
|
48
|
+
const URL_WITH_TOKEN = 'https://example.com/denylist.json?token=secret#frag';
|
|
49
|
+
const URL_WITH_CREDENTIALS = 'https://user:hunter2@example.com/denylist.json';
|
|
50
|
+
const REFRESH_INTERVAL_MS = 60_000;
|
|
51
|
+
|
|
52
|
+
const PARAMS: UserTransferQueryParams = {
|
|
53
|
+
routersByDomain: { 1: '0x67899664446ccfadf75e0d3f5668be262acd1069' },
|
|
54
|
+
excludeTxSenders: [],
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const INVALID_REGEX_FILE = JSON.stringify({
|
|
58
|
+
rules: [
|
|
59
|
+
{
|
|
60
|
+
reason: 'r',
|
|
61
|
+
addedBy: 'a',
|
|
62
|
+
match: { originDomain: 1, bodyRegex: '(' },
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const OVERSIZED_REGEX_FILE = JSON.stringify({
|
|
68
|
+
rules: [
|
|
69
|
+
{
|
|
70
|
+
reason: 'r',
|
|
71
|
+
addedBy: 'a',
|
|
72
|
+
match: { originDomain: 1, bodyRegex: '.?'.repeat(10000) },
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
function buildMessage(msgId: string): ExplorerMessage {
|
|
78
|
+
return {
|
|
79
|
+
msg_id: msgId,
|
|
80
|
+
origin_domain_id: 42161,
|
|
81
|
+
destination_domain_id: 1,
|
|
82
|
+
sender: '0x867a19a5b257bcb99e1840de2ef852268efd5e46',
|
|
83
|
+
recipient: '0x67899664446ccfadf75e0d3f5668be262acd1069',
|
|
84
|
+
origin_tx_hash:
|
|
85
|
+
'0x5e3c1f6a3c1a7f86e0f2d5e9c9c1b8d1e1e7e2b6c4d8a9f0e1d2c3b4a5968778',
|
|
86
|
+
origin_tx_sender: '0x995a921d333029512aa166885856c8af760e9381',
|
|
87
|
+
origin_tx_recipient: '0x867a19a5b257bcb99e1840de2ef852268efd5e46',
|
|
88
|
+
is_delivered: false,
|
|
89
|
+
message_body: `0x${'00'.repeat(64)}`,
|
|
90
|
+
send_occurred_at: null,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function denylistFor(messageIds: string[]): string {
|
|
95
|
+
return JSON.stringify({
|
|
96
|
+
rules: messageIds.map((messageId) => ({
|
|
97
|
+
reason: 'stuck',
|
|
98
|
+
addedBy: 'tester',
|
|
99
|
+
match: { messageId },
|
|
100
|
+
})),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** A valid denylist of exactly `bytes` ASCII bytes. */
|
|
105
|
+
function denylistOfSize(bytes: number): string {
|
|
106
|
+
const withReason = (reason: string) =>
|
|
107
|
+
JSON.stringify({
|
|
108
|
+
rules: [{ reason, addedBy: 'tester', match: { messageId: MESSAGE_ID } }],
|
|
109
|
+
});
|
|
110
|
+
const text = withReason('x'.repeat(bytes - withReason('').length));
|
|
111
|
+
|
|
112
|
+
expect(text).to.have.length(bytes);
|
|
113
|
+
return text;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
describe('RelayerDenylistMiddleware', () => {
|
|
117
|
+
let clock: Sinon.SinonFakeTimers;
|
|
118
|
+
let dir: string;
|
|
119
|
+
let denylistPath: string;
|
|
120
|
+
let denylistUrl: string;
|
|
121
|
+
let fetchStub: Sinon.SinonStub | undefined;
|
|
122
|
+
let explorerMessages: ExplorerMessage[];
|
|
123
|
+
const inner: IExplorerClient = {
|
|
124
|
+
getInflightUserTransfers: async () => explorerMessages,
|
|
125
|
+
getInflightRebalanceActions: async () => explorerMessages,
|
|
126
|
+
};
|
|
127
|
+
let logger: Logger;
|
|
128
|
+
let warn: Sinon.SinonSpy;
|
|
129
|
+
let info: Sinon.SinonSpy;
|
|
130
|
+
const messages = [buildMessage(MESSAGE_ID), buildMessage(OTHER_MESSAGE_ID)];
|
|
131
|
+
|
|
132
|
+
beforeEach(() => {
|
|
133
|
+
clock = Sinon.useFakeTimers({
|
|
134
|
+
now: Date.parse('2026-11-04T00:00:00Z'),
|
|
135
|
+
toFake: ['Date'],
|
|
136
|
+
});
|
|
137
|
+
dir = mkdtempSync(join(tmpdir(), 'relayer-denylist-'));
|
|
138
|
+
denylistPath = join(dir, 'denylist.json');
|
|
139
|
+
denylistUrl = pathToFileURL(denylistPath).href;
|
|
140
|
+
logger = pino({ level: 'silent' });
|
|
141
|
+
warn = Sinon.spy(logger, 'warn');
|
|
142
|
+
info = Sinon.spy(logger, 'info');
|
|
143
|
+
explorerMessages = messages;
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
afterEach(() => {
|
|
147
|
+
clock.restore();
|
|
148
|
+
fetchStub?.restore();
|
|
149
|
+
fetchStub = undefined;
|
|
150
|
+
rmSync(dir, { recursive: true, force: true });
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
function writeDenylist(text: string): void {
|
|
154
|
+
writeFileSync(denylistPath, text);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function createMiddleware(
|
|
158
|
+
url = denylistUrl,
|
|
159
|
+
): Promise<RelayerDenylistMiddleware> {
|
|
160
|
+
return RelayerDenylistMiddleware.create(
|
|
161
|
+
inner,
|
|
162
|
+
{ url, refreshIntervalMs: REFRESH_INTERVAL_MS },
|
|
163
|
+
logger,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Replaces the Date-only clock with one that also fakes timeouts. */
|
|
168
|
+
function fakeTimeouts(): void {
|
|
169
|
+
clock.restore();
|
|
170
|
+
clock = Sinon.useFakeTimers({
|
|
171
|
+
toFake: ['Date', 'setTimeout', 'clearTimeout'],
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Ids of the Explorer messages the middleware drops from the query. */
|
|
176
|
+
async function queryIgnoredIds(
|
|
177
|
+
middleware: RelayerDenylistMiddleware,
|
|
178
|
+
): Promise<string[]> {
|
|
179
|
+
const kept = await middleware.getInflightUserTransfers(PARAMS, logger);
|
|
180
|
+
const keptIds = new Set(kept.map((msg) => msg.msg_id));
|
|
181
|
+
|
|
182
|
+
expect(
|
|
183
|
+
warn.calledWith(
|
|
184
|
+
Sinon.match.any,
|
|
185
|
+
PENDING_TRANSFER_MIDDLEWARE_FAILED_MESSAGE,
|
|
186
|
+
),
|
|
187
|
+
).to.be.false;
|
|
188
|
+
|
|
189
|
+
return explorerMessages
|
|
190
|
+
.map((msg) => msg.msg_id)
|
|
191
|
+
.filter((id) => !keptIds.has(id));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
it('redacts credentials, query and fragment from URLs', () => {
|
|
195
|
+
const redacted = redactUrl(`${URL_WITH_CREDENTIALS}?token=secret#frag`);
|
|
196
|
+
|
|
197
|
+
expect(redacted).to.equal('https://example.com/denylist.json');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('rejects a URL with credentials without printing them', async () => {
|
|
201
|
+
fetchStub = Sinon.stub(globalThis, 'fetch');
|
|
202
|
+
|
|
203
|
+
const result = createMiddleware(URL_WITH_CREDENTIALS);
|
|
204
|
+
|
|
205
|
+
await expect(result).to.be.rejectedWith(
|
|
206
|
+
'Relayer denylist url https://example.com/denylist.json must not contain credentials',
|
|
207
|
+
);
|
|
208
|
+
expect(fetchStub.called).to.be.false;
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('ignores the messages the loaded rules deny', async () => {
|
|
212
|
+
writeDenylist(denylistFor([MESSAGE_ID]));
|
|
213
|
+
const middleware = await createMiddleware();
|
|
214
|
+
|
|
215
|
+
const ignored = await queryIgnoredIds(middleware);
|
|
216
|
+
|
|
217
|
+
expect(ignored).to.deep.equal([MESSAGE_ID]);
|
|
218
|
+
expect(middleware.isIgnored(MESSAGE_ID)).to.be.true;
|
|
219
|
+
expect(
|
|
220
|
+
info.calledWith(
|
|
221
|
+
Sinon.match({ msgId: MESSAGE_ID, reason: 'stuck (added by tester)' }),
|
|
222
|
+
'Ignoring pending transfer',
|
|
223
|
+
),
|
|
224
|
+
).to.be.true;
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('passes rebalance messages through and checks them against message-id rules only', async () => {
|
|
228
|
+
const unseenId = `0x${'ab'.repeat(32)}`;
|
|
229
|
+
writeDenylist(
|
|
230
|
+
JSON.stringify({
|
|
231
|
+
rules: [
|
|
232
|
+
{
|
|
233
|
+
reason: 'settled',
|
|
234
|
+
addedBy: 'tester',
|
|
235
|
+
match: { messageId: [MESSAGE_ID, unseenId] },
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
reason: 'paused',
|
|
239
|
+
addedBy: 'tester',
|
|
240
|
+
match: { destinationDomain: 1 },
|
|
241
|
+
},
|
|
242
|
+
],
|
|
243
|
+
}),
|
|
244
|
+
);
|
|
245
|
+
const middleware = await createMiddleware();
|
|
246
|
+
|
|
247
|
+
const rows = await middleware.getInflightRebalanceActions(
|
|
248
|
+
REBALANCE_PARAMS,
|
|
249
|
+
logger,
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
expect(rows.map((msg) => msg.msg_id)).to.deep.equal([
|
|
253
|
+
MESSAGE_ID,
|
|
254
|
+
OTHER_MESSAGE_ID,
|
|
255
|
+
]);
|
|
256
|
+
expect(middleware.isIgnoredRebalanceAction(MESSAGE_ID)).to.be.true;
|
|
257
|
+
expect(middleware.isIgnoredRebalanceAction(unseenId)).to.be.true;
|
|
258
|
+
expect(middleware.isIgnoredRebalanceAction(OTHER_MESSAGE_ID)).to.be.false;
|
|
259
|
+
expect(middleware.isIgnored(MESSAGE_ID)).to.be.false;
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('starts and stops ignoring a rebalance message as rebalance queries refresh its rule', async () => {
|
|
263
|
+
writeDenylist(denylistFor([]));
|
|
264
|
+
const middleware = await createMiddleware();
|
|
265
|
+
const rowIds = async (): Promise<string[]> =>
|
|
266
|
+
(
|
|
267
|
+
await middleware.getInflightRebalanceActions(REBALANCE_PARAMS, logger)
|
|
268
|
+
).map((msg) => msg.msg_id);
|
|
269
|
+
|
|
270
|
+
expect(middleware.isIgnoredRebalanceAction(OTHER_MESSAGE_ID)).to.be.false;
|
|
271
|
+
|
|
272
|
+
writeDenylist(denylistFor([OTHER_MESSAGE_ID]));
|
|
273
|
+
clock.tick(REFRESH_INTERVAL_MS);
|
|
274
|
+
expect(await rowIds()).to.deep.equal([MESSAGE_ID, OTHER_MESSAGE_ID]);
|
|
275
|
+
expect(middleware.isIgnoredRebalanceAction(OTHER_MESSAGE_ID)).to.be.true;
|
|
276
|
+
|
|
277
|
+
writeDenylist(denylistFor([]));
|
|
278
|
+
clock.tick(REFRESH_INTERVAL_MS);
|
|
279
|
+
expect(await rowIds()).to.deep.equal([MESSAGE_ID, OTHER_MESSAGE_ID]);
|
|
280
|
+
expect(middleware.isIgnoredRebalanceAction(OTHER_MESSAGE_ID)).to.be.false;
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('stops ignoring a rebalance message once its rule expires', async () => {
|
|
284
|
+
writeDenylist(
|
|
285
|
+
JSON.stringify({
|
|
286
|
+
rules: [
|
|
287
|
+
{
|
|
288
|
+
reason: 'settled',
|
|
289
|
+
addedBy: 'tester',
|
|
290
|
+
expiresAt: '2026-11-04T00:00:01Z',
|
|
291
|
+
match: { messageId: MESSAGE_ID },
|
|
292
|
+
},
|
|
293
|
+
],
|
|
294
|
+
}),
|
|
295
|
+
);
|
|
296
|
+
const middleware = await createMiddleware();
|
|
297
|
+
|
|
298
|
+
expect(middleware.isIgnoredRebalanceAction(MESSAGE_ID)).to.be.true;
|
|
299
|
+
|
|
300
|
+
clock.tick(1_000);
|
|
301
|
+
|
|
302
|
+
expect(middleware.isIgnoredRebalanceAction(MESSAGE_ID)).to.be.false;
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it('loads the denylist over https', async () => {
|
|
306
|
+
fetchStub = Sinon.stub(globalThis, 'fetch').resolves(
|
|
307
|
+
new Response(denylistFor([OTHER_MESSAGE_ID])),
|
|
308
|
+
);
|
|
309
|
+
const middleware = await createMiddleware(URL_WITH_TOKEN);
|
|
310
|
+
|
|
311
|
+
const ignored = await queryIgnoredIds(middleware);
|
|
312
|
+
|
|
313
|
+
expect(ignored).to.deep.equal([OTHER_MESSAGE_ID]);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
interface StartupFailureCase {
|
|
317
|
+
name: string;
|
|
318
|
+
response: () => Promise<Response>;
|
|
319
|
+
failure: DenylistLoadFailure;
|
|
320
|
+
fetches: number;
|
|
321
|
+
}
|
|
322
|
+
const startupFailures: StartupFailureCase[] = [
|
|
323
|
+
{
|
|
324
|
+
name: 'the fetch fails',
|
|
325
|
+
response: () =>
|
|
326
|
+
Promise.reject(new Error(`connect ECONNREFUSED ${URL_WITH_TOKEN}`)),
|
|
327
|
+
failure: DenylistLoadFailure.FetchFailed,
|
|
328
|
+
fetches: 3,
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
name: 'the file is invalid',
|
|
332
|
+
response: async () => new Response('{"rules": [{}]}'),
|
|
333
|
+
failure: DenylistLoadFailure.InvalidFile,
|
|
334
|
+
fetches: 1,
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
name: 'a bodyRegex does not compile',
|
|
338
|
+
response: async () => new Response(INVALID_REGEX_FILE),
|
|
339
|
+
failure: DenylistLoadFailure.InvalidFile,
|
|
340
|
+
fetches: 1,
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
name: 'the file starts with a BOM',
|
|
344
|
+
response: async () => new Response(`\ufeff${denylistFor([MESSAGE_ID])}`),
|
|
345
|
+
failure: DenylistLoadFailure.InvalidFile,
|
|
346
|
+
fetches: 1,
|
|
347
|
+
},
|
|
348
|
+
];
|
|
349
|
+
for (const c of startupFailures) {
|
|
350
|
+
it(`fails to start with a redacted URL when ${c.name}`, async () => {
|
|
351
|
+
fakeTimeouts();
|
|
352
|
+
fetchStub = Sinon.stub(globalThis, 'fetch').callsFake(c.response);
|
|
353
|
+
|
|
354
|
+
const result = createMiddleware(URL_WITH_TOKEN).then(
|
|
355
|
+
() => expect.fail('Expected create to throw'),
|
|
356
|
+
(err: unknown) => err,
|
|
357
|
+
);
|
|
358
|
+
await clock.tickAsync(2 * INITIAL_LOAD_RETRY_DELAY_MS);
|
|
359
|
+
const error = await result;
|
|
360
|
+
|
|
361
|
+
expect(fetchStub.callCount).to.equal(c.fetches);
|
|
362
|
+
expect(error).to.be.instanceOf(Error);
|
|
363
|
+
if (error instanceof Error) {
|
|
364
|
+
expect(error.message).to.match(
|
|
365
|
+
new RegExp(
|
|
366
|
+
`^Failed to load relayer denylist from https://example\\.com/denylist\\.json \\(${c.failure}\\): `,
|
|
367
|
+
),
|
|
368
|
+
);
|
|
369
|
+
expect(error.message).to.not.include('secret');
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
it('starts once a fetch succeeds after two failed attempts', async () => {
|
|
375
|
+
fakeTimeouts();
|
|
376
|
+
fetchStub = Sinon.stub(globalThis, 'fetch');
|
|
377
|
+
fetchStub.onCall(0).rejects(new Error('socket hang up'));
|
|
378
|
+
fetchStub.onCall(1).resolves(new Response('', { status: 503 }));
|
|
379
|
+
fetchStub.onCall(2).resolves(new Response(denylistFor([MESSAGE_ID])));
|
|
380
|
+
|
|
381
|
+
const result = createMiddleware(URL_WITH_TOKEN);
|
|
382
|
+
await clock.tickAsync(INITIAL_LOAD_RETRY_DELAY_MS - 1);
|
|
383
|
+
expect(fetchStub.callCount).to.equal(1);
|
|
384
|
+
await clock.tickAsync(1);
|
|
385
|
+
expect(fetchStub.callCount).to.equal(2);
|
|
386
|
+
await clock.tickAsync(INITIAL_LOAD_RETRY_DELAY_MS);
|
|
387
|
+
const middleware = await result;
|
|
388
|
+
|
|
389
|
+
expect(fetchStub.callCount).to.equal(3);
|
|
390
|
+
expect(await queryIgnoredIds(middleware)).to.deep.equal([MESSAGE_ID]);
|
|
391
|
+
expect(
|
|
392
|
+
warn
|
|
393
|
+
.getCalls()
|
|
394
|
+
.filter((call) =>
|
|
395
|
+
call.args.includes(RELAYER_DENYLIST_INITIAL_LOAD_RETRY_MESSAGE),
|
|
396
|
+
)
|
|
397
|
+
.map((call) => call.args[0]),
|
|
398
|
+
).to.deep.equal([
|
|
399
|
+
{
|
|
400
|
+
url: 'https://example.com/denylist.json',
|
|
401
|
+
attempt: 1,
|
|
402
|
+
error: 'socket hang up',
|
|
403
|
+
},
|
|
404
|
+
{
|
|
405
|
+
url: 'https://example.com/denylist.json',
|
|
406
|
+
attempt: 2,
|
|
407
|
+
error: 'HTTP status 503',
|
|
408
|
+
},
|
|
409
|
+
]);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it('refetches only once the refresh interval has passed', async () => {
|
|
413
|
+
writeDenylist(denylistFor([MESSAGE_ID]));
|
|
414
|
+
const middleware = await createMiddleware();
|
|
415
|
+
writeDenylist(denylistFor([OTHER_MESSAGE_ID]));
|
|
416
|
+
|
|
417
|
+
clock.tick(REFRESH_INTERVAL_MS - 1);
|
|
418
|
+
const withinInterval = await queryIgnoredIds(middleware);
|
|
419
|
+
|
|
420
|
+
expect(withinInterval).to.deep.equal([MESSAGE_ID]);
|
|
421
|
+
|
|
422
|
+
clock.tick(1);
|
|
423
|
+
const ignored = await queryIgnoredIds(middleware);
|
|
424
|
+
|
|
425
|
+
expect(ignored).to.deep.equal([OTHER_MESSAGE_ID]);
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
it('applies a refresh to a query that starts while it is in progress', async () => {
|
|
429
|
+
fetchStub = Sinon.stub(globalThis, 'fetch');
|
|
430
|
+
fetchStub
|
|
431
|
+
.onFirstCall()
|
|
432
|
+
.resolves(new Response(denylistFor([])))
|
|
433
|
+
.onSecondCall()
|
|
434
|
+
.resolves(new Response(denylistFor([MESSAGE_ID])));
|
|
435
|
+
const middleware = await createMiddleware(URL_WITH_TOKEN);
|
|
436
|
+
clock.tick(REFRESH_INTERVAL_MS);
|
|
437
|
+
|
|
438
|
+
const [transfers] = await Promise.all([
|
|
439
|
+
middleware.getInflightUserTransfers(PARAMS, logger),
|
|
440
|
+
middleware.getInflightRebalanceActions(REBALANCE_PARAMS, logger),
|
|
441
|
+
]);
|
|
442
|
+
|
|
443
|
+
expect(transfers.map((msg) => msg.msg_id)).to.deep.equal([
|
|
444
|
+
OTHER_MESSAGE_ID,
|
|
445
|
+
]);
|
|
446
|
+
expect(middleware.isIgnoredRebalanceAction(MESSAGE_ID)).to.be.true;
|
|
447
|
+
expect(fetchStub.callCount).to.equal(2);
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
interface RefreshFailureCase {
|
|
451
|
+
name: string;
|
|
452
|
+
breakDenylist: () => void;
|
|
453
|
+
failure: DenylistLoadFailure;
|
|
454
|
+
}
|
|
455
|
+
const refreshFailures: RefreshFailureCase[] = [
|
|
456
|
+
{
|
|
457
|
+
name: 'fetch failure',
|
|
458
|
+
breakDenylist: () => renameSync(denylistPath, `${denylistPath}.moved`),
|
|
459
|
+
failure: DenylistLoadFailure.FetchFailed,
|
|
460
|
+
},
|
|
461
|
+
{
|
|
462
|
+
name: 'invalid file',
|
|
463
|
+
breakDenylist: () => writeDenylist('not json'),
|
|
464
|
+
failure: DenylistLoadFailure.InvalidFile,
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
name: 'a bodyRegex that does not compile',
|
|
468
|
+
breakDenylist: () => writeDenylist(INVALID_REGEX_FILE),
|
|
469
|
+
failure: DenylistLoadFailure.InvalidFile,
|
|
470
|
+
},
|
|
471
|
+
{
|
|
472
|
+
name: "a bodyRegex past the relayer's size limit",
|
|
473
|
+
breakDenylist: () => writeDenylist(OVERSIZED_REGEX_FILE),
|
|
474
|
+
failure: DenylistLoadFailure.InvalidFile,
|
|
475
|
+
},
|
|
476
|
+
{
|
|
477
|
+
name: 'a file over the size limit',
|
|
478
|
+
breakDenylist: () =>
|
|
479
|
+
writeDenylist(denylistOfSize(MAX_DENYLIST_FILE_BYTES + 1)),
|
|
480
|
+
failure: DenylistLoadFailure.FetchFailed,
|
|
481
|
+
},
|
|
482
|
+
];
|
|
483
|
+
for (const c of refreshFailures) {
|
|
484
|
+
it(`keeps the last good rules and warns on ${c.name}`, async () => {
|
|
485
|
+
writeDenylist(denylistFor([MESSAGE_ID]));
|
|
486
|
+
const middleware = await createMiddleware();
|
|
487
|
+
c.breakDenylist();
|
|
488
|
+
clock.tick(REFRESH_INTERVAL_MS * 2);
|
|
489
|
+
|
|
490
|
+
const ignored = await queryIgnoredIds(middleware);
|
|
491
|
+
|
|
492
|
+
expect(ignored).to.deep.equal([MESSAGE_ID]);
|
|
493
|
+
expect(warn.callCount).to.equal(1);
|
|
494
|
+
const [fields, message] = warn.firstCall.args;
|
|
495
|
+
expect(message).to.equal(RELAYER_DENYLIST_REFRESH_FAILED_MESSAGE);
|
|
496
|
+
expect(fields).to.include({
|
|
497
|
+
url: denylistUrl,
|
|
498
|
+
failure: c.failure,
|
|
499
|
+
cachedRulesAgeSecs: 120,
|
|
500
|
+
});
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
it('redacts the URL and error of a failed https refresh', async () => {
|
|
505
|
+
fetchStub = Sinon.stub(globalThis, 'fetch');
|
|
506
|
+
fetchStub
|
|
507
|
+
.onFirstCall()
|
|
508
|
+
.resolves(new Response(denylistFor([MESSAGE_ID])))
|
|
509
|
+
.onSecondCall()
|
|
510
|
+
.rejects(new Error(`connect ECONNREFUSED ${URL_WITH_TOKEN}`));
|
|
511
|
+
const middleware = await createMiddleware(URL_WITH_TOKEN);
|
|
512
|
+
clock.tick(REFRESH_INTERVAL_MS);
|
|
513
|
+
|
|
514
|
+
const ignored = await queryIgnoredIds(middleware);
|
|
515
|
+
|
|
516
|
+
expect(ignored).to.deep.equal([MESSAGE_ID]);
|
|
517
|
+
expect(warn.callCount).to.equal(1);
|
|
518
|
+
const [fields, message] = warn.firstCall.args;
|
|
519
|
+
expect(message).to.equal(RELAYER_DENYLIST_REFRESH_FAILED_MESSAGE);
|
|
520
|
+
expect(fields).to.include({
|
|
521
|
+
url: 'https://example.com/denylist.json',
|
|
522
|
+
failure: DenylistLoadFailure.FetchFailed,
|
|
523
|
+
});
|
|
524
|
+
expect(fields.error).to.not.include('secret');
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
it('does not retry a failed refresh before the interval passes', async () => {
|
|
528
|
+
writeDenylist(denylistFor([MESSAGE_ID]));
|
|
529
|
+
const middleware = await createMiddleware();
|
|
530
|
+
writeDenylist('not json');
|
|
531
|
+
|
|
532
|
+
clock.tick(REFRESH_INTERVAL_MS);
|
|
533
|
+
await queryIgnoredIds(middleware);
|
|
534
|
+
writeDenylist(denylistFor([OTHER_MESSAGE_ID]));
|
|
535
|
+
clock.tick(1);
|
|
536
|
+
const ignored = await queryIgnoredIds(middleware);
|
|
537
|
+
|
|
538
|
+
expect(warn.callCount).to.equal(1);
|
|
539
|
+
expect(ignored).to.deep.equal([MESSAGE_ID]);
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
const unreadableSenders = [
|
|
543
|
+
{ name: 'a non-hex sender', sender: 'not-hex' },
|
|
544
|
+
{ name: 'a 20-byte zero sender', sender: `0x${'00'.repeat(20)}` },
|
|
545
|
+
];
|
|
546
|
+
for (const c of unreadableSenders) {
|
|
547
|
+
it(`keeps a message with ${c.name} and warns`, async () => {
|
|
548
|
+
writeDenylist(denylistFor([MESSAGE_ID]));
|
|
549
|
+
const middleware = await createMiddleware();
|
|
550
|
+
const message: ExplorerMessage = {
|
|
551
|
+
msg_id: MESSAGE_ID,
|
|
552
|
+
origin_domain_id: 42161,
|
|
553
|
+
destination_domain_id: 1,
|
|
554
|
+
sender: c.sender,
|
|
555
|
+
recipient: '0x67899664446ccfadf75e0d3f5668be262acd1069',
|
|
556
|
+
origin_tx_hash: '0x00',
|
|
557
|
+
origin_tx_sender: '0x995a921d333029512aa166885856c8af760e9381',
|
|
558
|
+
origin_tx_recipient: '0x867a19a5b257bcb99e1840de2ef852268efd5e46',
|
|
559
|
+
is_delivered: false,
|
|
560
|
+
message_body: '0x',
|
|
561
|
+
send_occurred_at: null,
|
|
562
|
+
};
|
|
563
|
+
explorerMessages = [message];
|
|
564
|
+
|
|
565
|
+
const ignored = await queryIgnoredIds(middleware);
|
|
566
|
+
|
|
567
|
+
expect(ignored).to.deep.equal([]);
|
|
568
|
+
expect(warn.callCount).to.equal(1);
|
|
569
|
+
const [fields, warning] = warn.firstCall.args;
|
|
570
|
+
expect(fields).to.include({ msgId: MESSAGE_ID });
|
|
571
|
+
expect(warning).to.equal(RELAYER_DENYLIST_CHECK_FAILED_MESSAGE);
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
describe('fetchDenylistFile', () => {
|
|
576
|
+
let dir: string;
|
|
577
|
+
let fetchStub: Sinon.SinonStub | undefined;
|
|
578
|
+
|
|
579
|
+
beforeEach(() => {
|
|
580
|
+
dir = mkdtempSync(join(tmpdir(), 'denylist-'));
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
afterEach(() => {
|
|
584
|
+
rmSync(dir, { recursive: true, force: true });
|
|
585
|
+
fetchStub?.restore();
|
|
586
|
+
fetchStub = undefined;
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
async function loadError(
|
|
590
|
+
result: Promise<string>,
|
|
591
|
+
): Promise<DenylistLoadError> {
|
|
592
|
+
const error = await result.then(
|
|
593
|
+
() => expect.fail('Expected fetchDenylistFile to throw'),
|
|
594
|
+
(err: unknown) => err,
|
|
595
|
+
);
|
|
596
|
+
assert(
|
|
597
|
+
error instanceof DenylistLoadError,
|
|
598
|
+
'Expected a DenylistLoadError',
|
|
599
|
+
);
|
|
600
|
+
return error;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
interface SourceCase {
|
|
604
|
+
name: string;
|
|
605
|
+
load: (bytes: Uint8Array<ArrayBuffer>) => Promise<string>;
|
|
606
|
+
}
|
|
607
|
+
const sources: SourceCase[] = [
|
|
608
|
+
{
|
|
609
|
+
name: 'file',
|
|
610
|
+
load: (bytes) => {
|
|
611
|
+
const path = join(dir, 'denylist.json');
|
|
612
|
+
writeFileSync(path, bytes);
|
|
613
|
+
return fetchDenylistFile(pathToFileURL(path));
|
|
614
|
+
},
|
|
615
|
+
},
|
|
616
|
+
{
|
|
617
|
+
name: 'http body',
|
|
618
|
+
load: (bytes) => {
|
|
619
|
+
fetchStub = Sinon.stub(globalThis, 'fetch').resolves(
|
|
620
|
+
new Response(bytes),
|
|
621
|
+
);
|
|
622
|
+
return fetchDenylistFile(
|
|
623
|
+
new URL('https://example.com/denylist.json'),
|
|
624
|
+
);
|
|
625
|
+
},
|
|
626
|
+
},
|
|
627
|
+
];
|
|
628
|
+
for (const c of sources) {
|
|
629
|
+
it(`reads a ${c.name} at the size limit`, async () => {
|
|
630
|
+
const denylist = denylistOfSize(MAX_DENYLIST_FILE_BYTES);
|
|
631
|
+
|
|
632
|
+
const text = await c.load(Buffer.from(denylist));
|
|
633
|
+
|
|
634
|
+
expect(text).to.equal(denylist);
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
it(`rejects a ${c.name} over the size limit`, async () => {
|
|
638
|
+
const denylist = denylistOfSize(MAX_DENYLIST_FILE_BYTES + 1);
|
|
639
|
+
|
|
640
|
+
const error = await loadError(c.load(Buffer.from(denylist)));
|
|
641
|
+
|
|
642
|
+
expect(error.failure).to.equal(DenylistLoadFailure.FetchFailed);
|
|
643
|
+
expect(error.message).to.equal(
|
|
644
|
+
'Denylist file is at least 4194305 bytes; the limit is 4194304',
|
|
645
|
+
);
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
it(`rejects a ${c.name} that is not UTF-8`, async () => {
|
|
649
|
+
const error = await loadError(c.load(new Uint8Array([0xff])));
|
|
650
|
+
|
|
651
|
+
expect(error.failure).to.equal(DenylistLoadFailure.InvalidFile);
|
|
652
|
+
expect(error.message).to.equal(
|
|
653
|
+
'Invalid denylist JSON: not valid UTF-8',
|
|
654
|
+
);
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
it(`keeps a leading BOM in a ${c.name}`, async () => {
|
|
658
|
+
const text = await c.load(Buffer.from(`\ufeff${denylistFor([])}`));
|
|
659
|
+
|
|
660
|
+
expect(text).to.equal(`\ufeff${denylistFor([])}`);
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
it('reads a body whose Content-Length is the limit', async () => {
|
|
665
|
+
const denylist = denylistOfSize(MAX_DENYLIST_FILE_BYTES);
|
|
666
|
+
fetchStub = Sinon.stub(globalThis, 'fetch').resolves(
|
|
667
|
+
new Response(Buffer.from(denylist), {
|
|
668
|
+
headers: { 'content-length': '4194304' },
|
|
669
|
+
}),
|
|
670
|
+
);
|
|
671
|
+
|
|
672
|
+
const text = await fetchDenylistFile(
|
|
673
|
+
new URL('https://example.com/denylist.json'),
|
|
674
|
+
);
|
|
675
|
+
|
|
676
|
+
expect(text).to.equal(denylist);
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
it('reads one byte past the limit of a larger file', async () => {
|
|
680
|
+
const path = join(dir, 'denylist.json');
|
|
681
|
+
writeFileSync(path, '');
|
|
682
|
+
truncateSync(path, 64 * 1024 * 1024);
|
|
683
|
+
|
|
684
|
+
const error = await loadError(fetchDenylistFile(pathToFileURL(path)));
|
|
685
|
+
|
|
686
|
+
expect(error.failure).to.equal(DenylistLoadFailure.FetchFailed);
|
|
687
|
+
expect(error.message).to.equal(
|
|
688
|
+
'Denylist file is at least 4194305 bytes; the limit is 4194304',
|
|
689
|
+
);
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
it('rejects a Content-Length over the limit before reading the body', async () => {
|
|
693
|
+
fetchStub = Sinon.stub(globalThis, 'fetch').resolves(
|
|
694
|
+
new Response(denylistFor([MESSAGE_ID]), {
|
|
695
|
+
headers: { 'content-length': '4194305' },
|
|
696
|
+
}),
|
|
697
|
+
);
|
|
698
|
+
|
|
699
|
+
const error = await loadError(
|
|
700
|
+
fetchDenylistFile(new URL('https://example.com/denylist.json')),
|
|
701
|
+
);
|
|
702
|
+
|
|
703
|
+
expect(error.failure).to.equal(DenylistLoadFailure.FetchFailed);
|
|
704
|
+
expect(error.message).to.equal(
|
|
705
|
+
'Denylist file is at least 4194305 bytes; the limit is 4194304',
|
|
706
|
+
);
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
it('stops reading a streamed body once it passes the limit', async () => {
|
|
710
|
+
const MiB = 1024 * 1024;
|
|
711
|
+
const chunkSizes = [MiB, MiB, MiB, MiB, 1, MiB];
|
|
712
|
+
let cancelled = false;
|
|
713
|
+
const body = new ReadableStream<Uint8Array>({
|
|
714
|
+
pull: (controller) => {
|
|
715
|
+
const size = chunkSizes.shift();
|
|
716
|
+
if (size === undefined) controller.close();
|
|
717
|
+
else controller.enqueue(new Uint8Array(size).fill(0x20));
|
|
718
|
+
},
|
|
719
|
+
cancel: () => {
|
|
720
|
+
cancelled = true;
|
|
721
|
+
},
|
|
722
|
+
});
|
|
723
|
+
fetchStub = Sinon.stub(globalThis, 'fetch').resolves(new Response(body));
|
|
724
|
+
|
|
725
|
+
const error = await loadError(
|
|
726
|
+
fetchDenylistFile(new URL('https://example.com/denylist.json')),
|
|
727
|
+
);
|
|
728
|
+
|
|
729
|
+
expect(error.failure).to.equal(DenylistLoadFailure.FetchFailed);
|
|
730
|
+
expect(error.message).to.equal(
|
|
731
|
+
'Denylist file is at least 4194305 bytes; the limit is 4194304',
|
|
732
|
+
);
|
|
733
|
+
expect(cancelled).to.be.true;
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
/** A response that sends its headers, then never sends the body. */
|
|
737
|
+
function stalledResponse(cancel: Sinon.SinonSpy): Response {
|
|
738
|
+
return new Response(
|
|
739
|
+
new ReadableStream<Uint8Array>({
|
|
740
|
+
pull: () => new Promise<void>(() => {}),
|
|
741
|
+
cancel,
|
|
742
|
+
}),
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
it('times out a body that stalls after the headers', async () => {
|
|
747
|
+
fakeTimeouts();
|
|
748
|
+
const cancel = Sinon.spy();
|
|
749
|
+
fetchStub = Sinon.stub(globalThis, 'fetch').resolves(
|
|
750
|
+
stalledResponse(cancel),
|
|
751
|
+
);
|
|
752
|
+
|
|
753
|
+
const result = loadError(
|
|
754
|
+
fetchDenylistFile(new URL('https://example.com/denylist.json')),
|
|
755
|
+
);
|
|
756
|
+
await clock.tickAsync(FETCH_TIMEOUT_MS);
|
|
757
|
+
const error = await result;
|
|
758
|
+
|
|
759
|
+
expect(error.failure).to.equal(DenylistLoadFailure.FetchFailed);
|
|
760
|
+
expect(error.message).to.equal('Denylist fetch timed out after 10000 ms');
|
|
761
|
+
expect(cancel.calledOnce).to.be.true;
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
it('times out a fetch that ignores the abort signal', async () => {
|
|
765
|
+
fakeTimeouts();
|
|
766
|
+
const stub = Sinon.stub(globalThis, 'fetch').returns(
|
|
767
|
+
new Promise<Response>(() => {}),
|
|
768
|
+
);
|
|
769
|
+
fetchStub = stub;
|
|
770
|
+
|
|
771
|
+
const result = loadError(
|
|
772
|
+
fetchDenylistFile(new URL('https://example.com/denylist.json')),
|
|
773
|
+
);
|
|
774
|
+
await clock.tickAsync(FETCH_TIMEOUT_MS);
|
|
775
|
+
const error = await result;
|
|
776
|
+
|
|
777
|
+
expect(error.failure).to.equal(DenylistLoadFailure.FetchFailed);
|
|
778
|
+
expect(error.message).to.equal('Denylist fetch timed out after 10000 ms');
|
|
779
|
+
expect(stub.firstCall.args[1]?.signal?.aborted).to.be.true;
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
it('cancels a body whose headers arrive after the timeout', async () => {
|
|
783
|
+
fakeTimeouts();
|
|
784
|
+
const cancel = Sinon.spy();
|
|
785
|
+
fetchStub = Sinon.stub(globalThis, 'fetch').callsFake(
|
|
786
|
+
() =>
|
|
787
|
+
new Promise<Response>((resolve) =>
|
|
788
|
+
setTimeout(
|
|
789
|
+
() => resolve(stalledResponse(cancel)),
|
|
790
|
+
FETCH_TIMEOUT_MS + 1000,
|
|
791
|
+
),
|
|
792
|
+
),
|
|
793
|
+
);
|
|
794
|
+
|
|
795
|
+
const result = loadError(
|
|
796
|
+
fetchDenylistFile(new URL('https://example.com/denylist.json')),
|
|
797
|
+
);
|
|
798
|
+
await clock.tickAsync(FETCH_TIMEOUT_MS);
|
|
799
|
+
const error = await result;
|
|
800
|
+
await clock.tickAsync(1000);
|
|
801
|
+
|
|
802
|
+
expect(error.message).to.equal('Denylist fetch timed out after 10000 ms');
|
|
803
|
+
expect(cancel.calledOnce).to.be.true;
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
it('reads file:// URLs', async () => {
|
|
807
|
+
const path = join(dir, 'denylist.json');
|
|
808
|
+
writeFileSync(path, denylistFor([MESSAGE_ID]));
|
|
809
|
+
|
|
810
|
+
const text = await fetchDenylistFile(pathToFileURL(path));
|
|
811
|
+
|
|
812
|
+
expect(text).to.equal(denylistFor([MESSAGE_ID]));
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
it('reads http(s):// URLs', async () => {
|
|
816
|
+
fetchStub = Sinon.stub(globalThis, 'fetch').resolves(
|
|
817
|
+
new Response(denylistFor([MESSAGE_ID])),
|
|
818
|
+
);
|
|
819
|
+
|
|
820
|
+
const text = await fetchDenylistFile(
|
|
821
|
+
new URL('https://example.com/denylist.json'),
|
|
822
|
+
);
|
|
823
|
+
|
|
824
|
+
expect(text).to.equal(denylistFor([MESSAGE_ID]));
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
it('reports a missing file as a fetch failure', async () => {
|
|
828
|
+
const result = fetchDenylistFile(
|
|
829
|
+
pathToFileURL(join(dir, 'missing.json')),
|
|
830
|
+
);
|
|
831
|
+
|
|
832
|
+
await expect(result).to.be.rejected.and.eventually.have.property(
|
|
833
|
+
'failure',
|
|
834
|
+
DenylistLoadFailure.FetchFailed,
|
|
835
|
+
);
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
it('keeps credentials out of the error when fetch rejects the URL', async () => {
|
|
839
|
+
const error = await fetchDenylistFile(new URL(URL_WITH_CREDENTIALS)).then(
|
|
840
|
+
() => expect.fail('Expected fetchDenylistFile to throw'),
|
|
841
|
+
(err: unknown) => err,
|
|
842
|
+
);
|
|
843
|
+
|
|
844
|
+
expect(error).to.be.instanceOf(DenylistLoadError);
|
|
845
|
+
if (error instanceof DenylistLoadError) {
|
|
846
|
+
expect(error.failure).to.equal(DenylistLoadFailure.FetchFailed);
|
|
847
|
+
expect(error.message).to.include('https://example.com/denylist.json');
|
|
848
|
+
expect(error.message).to.not.include('hunter2');
|
|
849
|
+
expect(error.message).to.not.include('user');
|
|
850
|
+
}
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
it('keeps query tokens out of the error when fetch fails', async () => {
|
|
854
|
+
fetchStub = Sinon.stub(globalThis, 'fetch').rejects(
|
|
855
|
+
new Error(`connect ECONNREFUSED ${URL_WITH_TOKEN}`),
|
|
856
|
+
);
|
|
857
|
+
|
|
858
|
+
const error = await fetchDenylistFile(new URL(URL_WITH_TOKEN)).then(
|
|
859
|
+
() => expect.fail('Expected fetchDenylistFile to throw'),
|
|
860
|
+
(err: unknown) => err,
|
|
861
|
+
);
|
|
862
|
+
|
|
863
|
+
expect(error).to.be.instanceOf(DenylistLoadError);
|
|
864
|
+
if (error instanceof DenylistLoadError) {
|
|
865
|
+
expect(error.message).to.equal(
|
|
866
|
+
'connect ECONNREFUSED https://example.com/denylist.json',
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
it('reports a non-2xx response as a fetch failure', async () => {
|
|
872
|
+
fetchStub = Sinon.stub(globalThis, 'fetch').resolves(
|
|
873
|
+
new Response('nope', { status: 404 }),
|
|
874
|
+
);
|
|
875
|
+
|
|
876
|
+
const result = fetchDenylistFile(
|
|
877
|
+
new URL('https://example.com/denylist.json'),
|
|
878
|
+
);
|
|
879
|
+
|
|
880
|
+
await expect(result).to.be.rejected.and.eventually.have.property(
|
|
881
|
+
'failure',
|
|
882
|
+
DenylistLoadFailure.FetchFailed,
|
|
883
|
+
);
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
it('reports an unsupported protocol as a fetch failure', async () => {
|
|
887
|
+
const result = fetchDenylistFile(new URL('ftp://example.com/x.json'));
|
|
888
|
+
|
|
889
|
+
await expect(result).to.be.rejectedWith(
|
|
890
|
+
'Unsupported denylist URL protocol ftp:',
|
|
891
|
+
);
|
|
892
|
+
});
|
|
893
|
+
});
|
|
894
|
+
});
|