@humanode-hyperlane/rebalancer 27.2.2 → 1029.1.1
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,1220 @@
|
|
|
1
|
+
import { expect } from 'chai';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
|
|
4
|
+
import { ZERO_ADDRESS_HEX_32 } from '@humanode-hyperlane/utils';
|
|
5
|
+
|
|
6
|
+
import type { ExplorerMessage } from '../../utils/ExplorerClient.js';
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
DenylistLoadError,
|
|
10
|
+
DenylistLoadFailure,
|
|
11
|
+
findDenyReason,
|
|
12
|
+
findMessageIdDenyReason,
|
|
13
|
+
parseDenylistAddress,
|
|
14
|
+
parseDenylistFile,
|
|
15
|
+
parseRfc3339,
|
|
16
|
+
} from './relayerDenylistRules.js';
|
|
17
|
+
|
|
18
|
+
const MESSAGE_ID =
|
|
19
|
+
'0x8960c5e41eb0d91973140ebc196892f5f07f512dd86047e8536e71424462c5d3';
|
|
20
|
+
const OTHER_MESSAGE_ID =
|
|
21
|
+
'0xdae65a8dd678bf8ea03faed7d53b99e52f5bdbae7c95834eff009cc37a18103c';
|
|
22
|
+
const ORIGIN_ROUTER = '0x867a19a5b257bcb99e1840de2ef852268efd5e46';
|
|
23
|
+
const DESTINATION_ROUTER = '0x67899664446ccfadf75e0d3f5668be262acd1069';
|
|
24
|
+
const TX_SENDER = '0x995a921d333029512aa166885856c8af760e9381';
|
|
25
|
+
const WARP_RECIPIENT = '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48';
|
|
26
|
+
const UNRELATED_ADDRESS = '0x1111111111111111111111111111111111111111';
|
|
27
|
+
const BODY_ADDRESS = '0xA0e768A68ba1BFffb9F4366dfC8D9195EE7217d1';
|
|
28
|
+
const TRON_ADDRESS = 'TG5iBMQPrDzJJAUM8GMVBf1hRaJ1wvzsGi';
|
|
29
|
+
const TRON_ADDRESS_BAD_CHECKSUM = 'TG5iBMQQf6E7GJWPU1eU9qdP9oQw4wKo3L';
|
|
30
|
+
const SOLANA_ADDRESS = 'AffzFDKZbyMiU11zmVBoAqD25ugpr7xoWWyRyGigBEaU';
|
|
31
|
+
const NOW = Date.parse('2026-11-04T00:00:00Z');
|
|
32
|
+
// From rust/main/app-contexts/mainnet_config.json.
|
|
33
|
+
const MAINNET_BODY_PREFIX =
|
|
34
|
+
'0100000000000000000000000001d40099fcd87c018969b0e8d4ab1633fb34763c';
|
|
35
|
+
const MAINNET_BODY_REGEX = `^${MAINNET_BODY_PREFIX}.{64}`;
|
|
36
|
+
|
|
37
|
+
function buildMessage(
|
|
38
|
+
fields: Pick<ExplorerMessage, 'msg_id'> &
|
|
39
|
+
Partial<
|
|
40
|
+
Pick<
|
|
41
|
+
ExplorerMessage,
|
|
42
|
+
| 'origin_domain_id'
|
|
43
|
+
| 'destination_domain_id'
|
|
44
|
+
| 'sender'
|
|
45
|
+
| 'recipient'
|
|
46
|
+
| 'origin_tx_sender'
|
|
47
|
+
| 'message_body'
|
|
48
|
+
>
|
|
49
|
+
>,
|
|
50
|
+
): ExplorerMessage {
|
|
51
|
+
return {
|
|
52
|
+
msg_id: fields.msg_id,
|
|
53
|
+
origin_domain_id: fields.origin_domain_id ?? 42161,
|
|
54
|
+
destination_domain_id: fields.destination_domain_id ?? 1,
|
|
55
|
+
sender: fields.sender ?? ORIGIN_ROUTER,
|
|
56
|
+
recipient: fields.recipient ?? DESTINATION_ROUTER,
|
|
57
|
+
origin_tx_hash:
|
|
58
|
+
'0x5e3c1f6a3c1a7f86e0f2d5e9c9c1b8d1e1e7e2b6c4d8a9f0e1d2c3b4a5968778',
|
|
59
|
+
origin_tx_sender: fields.origin_tx_sender ?? TX_SENDER,
|
|
60
|
+
origin_tx_recipient: ORIGIN_ROUTER,
|
|
61
|
+
is_delivered: false,
|
|
62
|
+
message_body:
|
|
63
|
+
fields.message_body ??
|
|
64
|
+
`0x000000000000000000000000${WARP_RECIPIENT.slice(2)}${'0'.repeat(56)}05f5e100`,
|
|
65
|
+
send_occurred_at: null,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function rulesFile(rules: unknown[]): string {
|
|
70
|
+
return JSON.stringify({ rules });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const BASE_RULE = { reason: 'test', addedBy: 'tester' };
|
|
74
|
+
|
|
75
|
+
function captureLoadError(text: string): DenylistLoadError {
|
|
76
|
+
try {
|
|
77
|
+
parseDenylistFile(text);
|
|
78
|
+
} catch (error: unknown) {
|
|
79
|
+
if (error instanceof DenylistLoadError) return error;
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return expect.fail('Expected parseDenylistFile to throw');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
describe('relayerDenylistRules', () => {
|
|
87
|
+
describe('parseDenylistAddress', () => {
|
|
88
|
+
interface AddressCase {
|
|
89
|
+
name: string;
|
|
90
|
+
input: string;
|
|
91
|
+
expected: string;
|
|
92
|
+
}
|
|
93
|
+
const cases: AddressCase[] = [
|
|
94
|
+
{
|
|
95
|
+
name: '20-byte EVM hex',
|
|
96
|
+
input: '0x67899664446CCfAdf75e0D3F5668be262aCd1069',
|
|
97
|
+
expected:
|
|
98
|
+
'0x00000000000000000000000067899664446ccfadf75e0d3f5668be262acd1069',
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: '32-byte hex',
|
|
102
|
+
input:
|
|
103
|
+
'0x8960C5E41EB0D91973140EBC196892F5F07F512DD86047E8536E71424462C5D3',
|
|
104
|
+
expected: MESSAGE_ID,
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
name: 'Tron base58',
|
|
108
|
+
input: TRON_ADDRESS,
|
|
109
|
+
expected:
|
|
110
|
+
'0x000000000000000000000000430b9b4a60d18affe500b731a97b5aa622c7863f',
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
name: 'Solana base58',
|
|
114
|
+
input: SOLANA_ADDRESS,
|
|
115
|
+
expected:
|
|
116
|
+
'0x8fa155f74d49e7464faa0d27041225f2ec5b8a5632fc68efb022d57946ec19c9',
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: 'bech32',
|
|
120
|
+
input: 'cosmos1zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3pahzj0',
|
|
121
|
+
expected:
|
|
122
|
+
'0x0000000000000000000000001111111111111111111111111111111111111111',
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
name: '20-byte zero hex',
|
|
126
|
+
input: '0x0000000000000000000000000000000000000000',
|
|
127
|
+
expected: ZERO_ADDRESS_HEX_32,
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
name: 'bech32m',
|
|
131
|
+
input:
|
|
132
|
+
'account_rdx1cy3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zk3cl7y',
|
|
133
|
+
expected:
|
|
134
|
+
'0x0000c12222222222222222222222222222222222222222222222222222222222',
|
|
135
|
+
},
|
|
136
|
+
];
|
|
137
|
+
for (const c of cases) {
|
|
138
|
+
it(`parses ${c.name}`, () => {
|
|
139
|
+
expect(parseDenylistAddress(c.input)).to.equal(c.expected);
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const invalid = [
|
|
144
|
+
'0x1234',
|
|
145
|
+
'0xzz899664446ccfadf75e0d3f5668be262acd1069',
|
|
146
|
+
TRON_ADDRESS_BAD_CHECKSUM,
|
|
147
|
+
'*',
|
|
148
|
+
'not-an-address',
|
|
149
|
+
];
|
|
150
|
+
for (const input of invalid) {
|
|
151
|
+
it(`rejects ${input}`, () => {
|
|
152
|
+
expect(() => parseDenylistAddress(input)).to.throw();
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe('parseRfc3339', () => {
|
|
158
|
+
interface TimestampCase {
|
|
159
|
+
input: string;
|
|
160
|
+
expected: number | undefined;
|
|
161
|
+
}
|
|
162
|
+
const cases: TimestampCase[] = [
|
|
163
|
+
{ input: '2026-12-01T00:00:00Z', expected: 1796083200000 },
|
|
164
|
+
{ input: '2026-12-01 00:00:00Z', expected: 1796083200000 },
|
|
165
|
+
{ input: '2026-12-01t00:00:00z', expected: 1796083200000 },
|
|
166
|
+
{ input: '2026-12-01T01:30:00+01:30', expected: 1796083200000 },
|
|
167
|
+
{ input: '2026-11-30T23:00:00-01:00', expected: 1796083200000 },
|
|
168
|
+
{ input: '2026-12-01T00:00:00.5Z', expected: 1796083200500 },
|
|
169
|
+
{ input: '2028-02-29T00:00:00Z', expected: 1835395200000 },
|
|
170
|
+
{ input: 'soon', expected: undefined },
|
|
171
|
+
{ input: '2026-12-01T00:00Z', expected: undefined },
|
|
172
|
+
{ input: '2026-12-01T00:00:00', expected: undefined },
|
|
173
|
+
{ input: '2026-12-01_00:00:00Z', expected: undefined },
|
|
174
|
+
{ input: '2026-02-29T00:00:00Z', expected: undefined },
|
|
175
|
+
{ input: '2026-13-01T00:00:00Z', expected: undefined },
|
|
176
|
+
{ input: '2026-12-01T24:00:00Z', expected: undefined },
|
|
177
|
+
{ input: '2026-12-01T00:00:00+24:00', expected: undefined },
|
|
178
|
+
];
|
|
179
|
+
for (const c of cases) {
|
|
180
|
+
it(`parses ${c.input}`, () => {
|
|
181
|
+
expect(parseRfc3339(c.input)).to.equal(c.expected);
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
it('accepts expiresAt timestamps the relayer accepts', () => {
|
|
186
|
+
const rules = parseDenylistFile(
|
|
187
|
+
rulesFile([
|
|
188
|
+
{
|
|
189
|
+
reason: 'r',
|
|
190
|
+
addedBy: 'a',
|
|
191
|
+
expiresAt: '2026-12-01 00:00:00z',
|
|
192
|
+
address: TX_SENDER,
|
|
193
|
+
},
|
|
194
|
+
]),
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
expect(rules.addressRules[0].expiresAt).to.equal(1796083200000);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe('parseDenylistFile', () => {
|
|
202
|
+
it('accepts the live registry denylist, including matchBody rules', () => {
|
|
203
|
+
const text = readFileSync(
|
|
204
|
+
new URL('./fixtures/mainnet-denylist.json', import.meta.url),
|
|
205
|
+
'utf8',
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
const rules = parseDenylistFile(text);
|
|
209
|
+
|
|
210
|
+
expect(rules.matchRules).to.have.lengthOf(438);
|
|
211
|
+
expect(rules.addressRules).to.have.lengthOf(33);
|
|
212
|
+
|
|
213
|
+
const bodyAddresses = rules.addressRules
|
|
214
|
+
.map((rule) => rule.bodyAddress)
|
|
215
|
+
.filter((address) => address !== undefined);
|
|
216
|
+
|
|
217
|
+
expect(bodyAddresses).to.have.lengthOf(7);
|
|
218
|
+
expect(bodyAddresses).to.include(
|
|
219
|
+
'a0e768a68ba1bfffb9f4366dfc8d9195ee7217d1',
|
|
220
|
+
);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
interface InvalidFileCase {
|
|
224
|
+
name: string;
|
|
225
|
+
text: string;
|
|
226
|
+
failure: DenylistLoadFailure;
|
|
227
|
+
message: RegExp;
|
|
228
|
+
}
|
|
229
|
+
const invalidCases: InvalidFileCase[] = [
|
|
230
|
+
{
|
|
231
|
+
name: 'non-JSON',
|
|
232
|
+
text: '{',
|
|
233
|
+
failure: DenylistLoadFailure.InvalidFile,
|
|
234
|
+
message: /^Invalid denylist JSON/,
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
name: 'missing rules',
|
|
238
|
+
text: '{}',
|
|
239
|
+
failure: DenylistLoadFailure.InvalidFile,
|
|
240
|
+
message: /^file: rules:/,
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
name: 'unknown top-level key',
|
|
244
|
+
text: JSON.stringify({ rules: [], extra: 1 }),
|
|
245
|
+
failure: DenylistLoadFailure.InvalidFile,
|
|
246
|
+
message: /Unrecognized key: "extra"/,
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
name: 'both match and address',
|
|
250
|
+
text: rulesFile([
|
|
251
|
+
{
|
|
252
|
+
reason: 'r',
|
|
253
|
+
addedBy: 'a',
|
|
254
|
+
match: { messageId: MESSAGE_ID },
|
|
255
|
+
address: TX_SENDER,
|
|
256
|
+
},
|
|
257
|
+
]),
|
|
258
|
+
failure: DenylistLoadFailure.InvalidFile,
|
|
259
|
+
message: /Exactly one of `match` or `address` must be set/,
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
name: 'empty reason',
|
|
263
|
+
text: rulesFile([{ reason: ' ', addedBy: 'a', address: TX_SENDER }]),
|
|
264
|
+
failure: DenylistLoadFailure.InvalidFile,
|
|
265
|
+
message: /`reason` must not be empty/,
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
name: 'invalid expiresAt',
|
|
269
|
+
text: rulesFile([
|
|
270
|
+
{ reason: 'r', addedBy: 'a', expiresAt: 'soon', address: TX_SENDER },
|
|
271
|
+
]),
|
|
272
|
+
failure: DenylistLoadFailure.InvalidFile,
|
|
273
|
+
message:
|
|
274
|
+
/^Invalid denylist rule at index 0: rule: expiresAt: `expiresAt` must be an RFC 3339 timestamp/,
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
name: 'wildcard-only match',
|
|
278
|
+
text: rulesFile([
|
|
279
|
+
{ reason: 'r', addedBy: 'a', match: { originDomain: '*' } },
|
|
280
|
+
]),
|
|
281
|
+
failure: DenylistLoadFailure.InvalidFile,
|
|
282
|
+
message: /`match` must set messageId/,
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
name: 'empty list',
|
|
286
|
+
text: rulesFile([
|
|
287
|
+
{ reason: 'r', addedBy: 'a', match: { originDomain: [] } },
|
|
288
|
+
]),
|
|
289
|
+
failure: DenylistLoadFailure.InvalidFile,
|
|
290
|
+
message: /match.originDomain is an empty list/,
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
name: 'unknown match field',
|
|
294
|
+
text: rulesFile([
|
|
295
|
+
{ reason: 'r', addedBy: 'a', match: { nonce: 1, originDomain: 1 } },
|
|
296
|
+
]),
|
|
297
|
+
failure: DenylistLoadFailure.InvalidFile,
|
|
298
|
+
message: /Unknown `match` field `nonce`/,
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
name: 'conflicting match aliases',
|
|
302
|
+
text: rulesFile([
|
|
303
|
+
{
|
|
304
|
+
reason: 'r',
|
|
305
|
+
addedBy: 'a',
|
|
306
|
+
match: { originDomain: 1, origindomain: 2 },
|
|
307
|
+
},
|
|
308
|
+
]),
|
|
309
|
+
failure: DenylistLoadFailure.InvalidFile,
|
|
310
|
+
message: /Conflicting `match` aliases for `origindomain`/,
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
name: 'a 20-byte zero address',
|
|
314
|
+
text: rulesFile([
|
|
315
|
+
{
|
|
316
|
+
reason: 'r',
|
|
317
|
+
addedBy: 'a',
|
|
318
|
+
address: '0x0000000000000000000000000000000000000000',
|
|
319
|
+
},
|
|
320
|
+
]),
|
|
321
|
+
failure: DenylistLoadFailure.InvalidFile,
|
|
322
|
+
message: /`address` must not be zero/,
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
name: 'a 32-byte zero address',
|
|
326
|
+
text: rulesFile([
|
|
327
|
+
{
|
|
328
|
+
reason: 'r',
|
|
329
|
+
addedBy: 'a',
|
|
330
|
+
address: ZERO_ADDRESS_HEX_32,
|
|
331
|
+
},
|
|
332
|
+
]),
|
|
333
|
+
failure: DenylistLoadFailure.InvalidFile,
|
|
334
|
+
message: /`address` must not be zero/,
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
name: 'bad Tron checksum in match',
|
|
338
|
+
text: rulesFile([
|
|
339
|
+
{
|
|
340
|
+
reason: 'r',
|
|
341
|
+
addedBy: 'a',
|
|
342
|
+
match: { senderAddress: [TRON_ADDRESS_BAD_CHECKSUM] },
|
|
343
|
+
},
|
|
344
|
+
]),
|
|
345
|
+
failure: DenylistLoadFailure.InvalidFile,
|
|
346
|
+
message: /failed its prefix or checksum check/,
|
|
347
|
+
},
|
|
348
|
+
{
|
|
349
|
+
name: 'matchBody on a match rule',
|
|
350
|
+
text: rulesFile([
|
|
351
|
+
{
|
|
352
|
+
reason: 'r',
|
|
353
|
+
addedBy: 'a',
|
|
354
|
+
match: { originDomain: 1 },
|
|
355
|
+
matchBody: false,
|
|
356
|
+
},
|
|
357
|
+
]),
|
|
358
|
+
failure: DenylistLoadFailure.InvalidFile,
|
|
359
|
+
message: /`matchBody` is only valid on an `address` rule/,
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
name: 'a non-boolean matchBody',
|
|
363
|
+
text: rulesFile([
|
|
364
|
+
{ reason: 'r', addedBy: 'a', address: TX_SENDER, matchBody: 'true' },
|
|
365
|
+
]),
|
|
366
|
+
failure: DenylistLoadFailure.InvalidFile,
|
|
367
|
+
message: /^Invalid denylist rule at index 0: rule: matchBody: /,
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
name: 'matchBody on a 32-byte hex address',
|
|
371
|
+
text: rulesFile([
|
|
372
|
+
{
|
|
373
|
+
reason: 'r',
|
|
374
|
+
addedBy: 'a',
|
|
375
|
+
address: MESSAGE_ID,
|
|
376
|
+
matchBody: true,
|
|
377
|
+
},
|
|
378
|
+
]),
|
|
379
|
+
failure: DenylistLoadFailure.InvalidFile,
|
|
380
|
+
message: /`matchBody` requires a 20-byte EVM hex address/,
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
name: 'matchBody on a Tron address',
|
|
384
|
+
text: rulesFile([
|
|
385
|
+
{
|
|
386
|
+
reason: 'r',
|
|
387
|
+
addedBy: 'a',
|
|
388
|
+
address: TRON_ADDRESS,
|
|
389
|
+
matchBody: true,
|
|
390
|
+
},
|
|
391
|
+
]),
|
|
392
|
+
failure: DenylistLoadFailure.InvalidFile,
|
|
393
|
+
message: /`matchBody` requires a 20-byte EVM hex address/,
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
name: 'an empty bodyRegex alone',
|
|
397
|
+
text: rulesFile([
|
|
398
|
+
{ reason: 'r', addedBy: 'a', match: { bodyRegex: '' } },
|
|
399
|
+
]),
|
|
400
|
+
failure: DenylistLoadFailure.InvalidFile,
|
|
401
|
+
message: /`match` must set messageId/,
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
name: 'a match-all bodyRegex alone',
|
|
405
|
+
text: rulesFile([
|
|
406
|
+
{ reason: 'r', addedBy: 'a', match: { bodyRegex: '.*' } },
|
|
407
|
+
]),
|
|
408
|
+
failure: DenylistLoadFailure.InvalidFile,
|
|
409
|
+
message: /`match` must set messageId/,
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
name: 'a bodyRegex that does not compile',
|
|
413
|
+
text: rulesFile([
|
|
414
|
+
{
|
|
415
|
+
reason: 'r',
|
|
416
|
+
addedBy: 'a',
|
|
417
|
+
match: { originDomain: 1, bodyRegex: '(' },
|
|
418
|
+
},
|
|
419
|
+
]),
|
|
420
|
+
failure: DenylistLoadFailure.InvalidFile,
|
|
421
|
+
message: /match.bodyRegex is not a valid regex/,
|
|
422
|
+
},
|
|
423
|
+
...[
|
|
424
|
+
['a digit escape', 'a\\0'],
|
|
425
|
+
['a digit escape in a class', '[\\0]'],
|
|
426
|
+
['a backreference', '(ab)\\1'],
|
|
427
|
+
['a quoted sequence', '\\Q.\\E'],
|
|
428
|
+
['a start-of-word escape', '\\<a'],
|
|
429
|
+
['an end-of-word escape', 'a\\>'],
|
|
430
|
+
['a word boundary with braces', '\\b{start}'],
|
|
431
|
+
['a non-word boundary with braces', '\\B{x}'],
|
|
432
|
+
['a count without a minimum', 'a{,3}'],
|
|
433
|
+
['a count with a space', 'a{ 2}'],
|
|
434
|
+
['a count with a leading zero', 'a{02}'],
|
|
435
|
+
['an unclosed brace', 'a{'],
|
|
436
|
+
['a group name starting with a digit', '(?P<1n>a)'],
|
|
437
|
+
['a short group name starting with a digit', '(?<1n>a)'],
|
|
438
|
+
['a quantifier after a flag group', '(?i)*'],
|
|
439
|
+
['a count after a flag group', '(?i){2}'],
|
|
440
|
+
['a class intersection', '[a-f&&b-f]'],
|
|
441
|
+
['the mainnet prefix with a class intersection', '^0{24}[a-f&&b-f]'],
|
|
442
|
+
['a class difference', '[a-f--b]'],
|
|
443
|
+
['a class symmetric difference', '[a-f~~b]'],
|
|
444
|
+
['a nested class', '[[a-c]d]'],
|
|
445
|
+
['a range ending in a class', '[ -[:alpha:]]'],
|
|
446
|
+
['a range starting at an escaped class', '[\\d-z]'],
|
|
447
|
+
['a range starting at a braced Unicode class', '[\\p{L}-z]'],
|
|
448
|
+
['groups nested 61 deep', `${'('.repeat(61)}a${')'.repeat(61)}`],
|
|
449
|
+
['a surrogate escape', '\\x{d800}'],
|
|
450
|
+
['the last surrogate', '\\x{dfff}'],
|
|
451
|
+
['an uppercase surrogate escape', '\\x{D800}'],
|
|
452
|
+
['a surrogate range end', '[a-\\x{d8c1}]'],
|
|
453
|
+
['a negated Unicode property', '\\p{^Greek}'],
|
|
454
|
+
['a negated Unicode property in a class', '[\\P{^L}]'],
|
|
455
|
+
].map(([syntax, bodyRegex]) => ({
|
|
456
|
+
name: `a bodyRegex with ${syntax}`,
|
|
457
|
+
text: rulesFile([
|
|
458
|
+
{ reason: 'r', addedBy: 'a', match: { originDomain: 1, bodyRegex } },
|
|
459
|
+
]),
|
|
460
|
+
failure: DenylistLoadFailure.InvalidFile,
|
|
461
|
+
message:
|
|
462
|
+
/syntax the relayer's regex engine rejects or reads differently/,
|
|
463
|
+
})),
|
|
464
|
+
...[
|
|
465
|
+
['lookahead', '(?=ab)ab'],
|
|
466
|
+
['lookbehind', '(?<=a)b'],
|
|
467
|
+
['negative lookbehind', '(?<!a)b'],
|
|
468
|
+
['a named backreference', '(?<n>a)\\k<n>'],
|
|
469
|
+
['a control escape', '\\cA'],
|
|
470
|
+
['an unclosed class opened by ]', '[]a'],
|
|
471
|
+
['an unclosed negated class opened by ]', '[^]'],
|
|
472
|
+
['a braced Unicode escape', '\\u{41}'],
|
|
473
|
+
['the verbose flag', '(?x) a'],
|
|
474
|
+
['stacked quantifiers', 'a**'],
|
|
475
|
+
['a quantifier after a count', 'a{2}*'],
|
|
476
|
+
['a count above 1000', 'a{1001}'],
|
|
477
|
+
].map(([syntax, bodyRegex]) => ({
|
|
478
|
+
name: `a bodyRegex with ${syntax}`,
|
|
479
|
+
text: rulesFile([
|
|
480
|
+
{ reason: 'r', addedBy: 'a', match: { originDomain: 1, bodyRegex } },
|
|
481
|
+
]),
|
|
482
|
+
failure: DenylistLoadFailure.InvalidFile,
|
|
483
|
+
message: /match.bodyRegex is not a valid regex/,
|
|
484
|
+
})),
|
|
485
|
+
...[
|
|
486
|
+
['a non-digit class past the relayer size limit', '\\D{900}'],
|
|
487
|
+
['a word class past the relayer size limit', '\\w{220}'],
|
|
488
|
+
['repeats adding up past the relayer size limit', '\\w{150}\\w{150}'],
|
|
489
|
+
['a repeated group past the relayer size limit', '(?:\\w{50}){5}'],
|
|
490
|
+
['a negated digit class past the relayer size limit', '[^\\d]{900}'],
|
|
491
|
+
[
|
|
492
|
+
'negated space classes adding up past the relayer size limit',
|
|
493
|
+
'[^\\s]{1000}'.repeat(6),
|
|
494
|
+
],
|
|
495
|
+
[
|
|
496
|
+
'negated literal classes adding up past the relayer size limit',
|
|
497
|
+
'[^\\x{10000}]{1000}'.repeat(10),
|
|
498
|
+
],
|
|
499
|
+
[
|
|
500
|
+
'case-folded negated ranges adding up past the relayer size limit',
|
|
501
|
+
'(?i)[^\\x{1e00}-\\x{a7ff}]{1000}'.repeat(3),
|
|
502
|
+
],
|
|
503
|
+
[
|
|
504
|
+
'a negated class of 1000 members past the relayer size limit',
|
|
505
|
+
`[^${Array.from({ length: 1000 }, (_, i) => `\\x{${(0xe000 + 97 * i).toString(16)}}`).join('')}]{100}`,
|
|
506
|
+
],
|
|
507
|
+
[
|
|
508
|
+
'a repeated group past the conservative size estimate',
|
|
509
|
+
'(?:\\w{51})+',
|
|
510
|
+
],
|
|
511
|
+
[
|
|
512
|
+
'an open-ended count past the conservative size estimate',
|
|
513
|
+
'\\w{100,}',
|
|
514
|
+
],
|
|
515
|
+
['alternatives past the conservative size estimate', '\\w{60}|\\w{60}'],
|
|
516
|
+
['alternatives just past the size estimate', '\\w{50}|\\w{50}'],
|
|
517
|
+
['captures past the size estimate', '(a)'.repeat(5001)],
|
|
518
|
+
[
|
|
519
|
+
'named captures past the size estimate',
|
|
520
|
+
Array.from({ length: 5001 }, (_, i) => `(?<n${i}>a)`).join(''),
|
|
521
|
+
],
|
|
522
|
+
[
|
|
523
|
+
'nested captures past the relayer size limit',
|
|
524
|
+
('('.repeat(60) + '.' + ')'.repeat(60)).repeat(3000),
|
|
525
|
+
],
|
|
526
|
+
[
|
|
527
|
+
'empty alternatives past the relayer size limit',
|
|
528
|
+
'a' + '|'.repeat(300000),
|
|
529
|
+
],
|
|
530
|
+
[
|
|
531
|
+
'empty alternative groups past the relayer size limit',
|
|
532
|
+
'(?:|)'.repeat(80000),
|
|
533
|
+
],
|
|
534
|
+
['optionals past the relayer size limit', '.?'.repeat(10000)],
|
|
535
|
+
[
|
|
536
|
+
'zero-or-one counts past the relayer size limit',
|
|
537
|
+
'.{0,1}'.repeat(10000),
|
|
538
|
+
],
|
|
539
|
+
[
|
|
540
|
+
'repeated optionals past the relayer size limit',
|
|
541
|
+
'(?:.?){1000}'.repeat(10),
|
|
542
|
+
],
|
|
543
|
+
[
|
|
544
|
+
'optional empty groups past the relayer size limit',
|
|
545
|
+
'(?:)?'.repeat(150000),
|
|
546
|
+
],
|
|
547
|
+
[
|
|
548
|
+
'starred empty groups past the relayer size limit',
|
|
549
|
+
'(?:)*'.repeat(150000),
|
|
550
|
+
],
|
|
551
|
+
[
|
|
552
|
+
'open-ended zero counts past the relayer size limit',
|
|
553
|
+
'(?:){0,}'.repeat(150000),
|
|
554
|
+
],
|
|
555
|
+
[
|
|
556
|
+
'nested optionals past the relayer size limit',
|
|
557
|
+
('(?:'.repeat(59) + 'a?' + ')?'.repeat(59)).repeat(5000),
|
|
558
|
+
],
|
|
559
|
+
[
|
|
560
|
+
'lazy optionals past the relayer size limit',
|
|
561
|
+
'(?:.??){1000}'.repeat(10),
|
|
562
|
+
],
|
|
563
|
+
[
|
|
564
|
+
'repeated zero-or-one counts past the relayer size limit',
|
|
565
|
+
'(?:.{0,1}){1000}'.repeat(10),
|
|
566
|
+
],
|
|
567
|
+
[
|
|
568
|
+
'a closing group past the conservative size estimate',
|
|
569
|
+
'\\D{100}(?:\\D{100})',
|
|
570
|
+
],
|
|
571
|
+
[
|
|
572
|
+
'nested closing groups past the size limit',
|
|
573
|
+
'\\D{100}' + '(?:\\D{100}'.repeat(8) + ')'.repeat(8),
|
|
574
|
+
],
|
|
575
|
+
].map(([syntax, bodyRegex]) => ({
|
|
576
|
+
name: `a bodyRegex with ${syntax}`,
|
|
577
|
+
text: rulesFile([
|
|
578
|
+
{ reason: 'r', addedBy: 'a', match: { originDomain: 1, bodyRegex } },
|
|
579
|
+
]),
|
|
580
|
+
failure: DenylistLoadFailure.InvalidFile,
|
|
581
|
+
message: /may exceed the relayer's regex size limit/,
|
|
582
|
+
})),
|
|
583
|
+
{
|
|
584
|
+
name: 'a non-string bodyRegex',
|
|
585
|
+
text: rulesFile([
|
|
586
|
+
{
|
|
587
|
+
reason: 'r',
|
|
588
|
+
addedBy: 'a',
|
|
589
|
+
match: { originDomain: 1, bodyRegex: 1 },
|
|
590
|
+
},
|
|
591
|
+
]),
|
|
592
|
+
failure: DenylistLoadFailure.InvalidFile,
|
|
593
|
+
message: /match.bodyRegex: /,
|
|
594
|
+
},
|
|
595
|
+
{
|
|
596
|
+
name: 'conflicting bodyRegex aliases',
|
|
597
|
+
text: rulesFile([
|
|
598
|
+
{
|
|
599
|
+
reason: 'r',
|
|
600
|
+
addedBy: 'a',
|
|
601
|
+
match: { originDomain: 1, bodyRegex: '\\D', BODYREGEX: '\\d' },
|
|
602
|
+
},
|
|
603
|
+
]),
|
|
604
|
+
failure: DenylistLoadFailure.InvalidFile,
|
|
605
|
+
message: /Conflicting `match` aliases for `bodyregex`/,
|
|
606
|
+
},
|
|
607
|
+
];
|
|
608
|
+
for (const c of invalidCases) {
|
|
609
|
+
it(`rejects the whole file on ${c.name}`, () => {
|
|
610
|
+
const error = captureLoadError(c.text);
|
|
611
|
+
|
|
612
|
+
expect(error.failure).to.equal(c.failure);
|
|
613
|
+
expect(error.message).to.match(c.message);
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/** Raw text, since `JSON.stringify` would rewrite `1.0` as `1`. */
|
|
618
|
+
function domainFile(literal: string): string {
|
|
619
|
+
return `{"rules":[{"reason":"r","addedBy":"a","match":{"originDomain":${literal}}}]}`;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// The relayer's serde parser, for both a single domain and a list.
|
|
623
|
+
const acceptedDomains: Array<[string, number]> = [
|
|
624
|
+
['"+5"', 5],
|
|
625
|
+
['"007"', 7],
|
|
626
|
+
['4294967295', 4294967295],
|
|
627
|
+
['"4294967295"', 4294967295],
|
|
628
|
+
];
|
|
629
|
+
for (const [literal, domain] of acceptedDomains) {
|
|
630
|
+
for (const value of [literal, `[${literal}]`]) {
|
|
631
|
+
it(`accepts the domain ${value}`, () => {
|
|
632
|
+
const rules = parseDenylistFile(domainFile(value));
|
|
633
|
+
|
|
634
|
+
expect([...(rules.matchRules[0].originDomains ?? [])]).to.deep.equal([
|
|
635
|
+
domain,
|
|
636
|
+
]);
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
const rejectedDomains: Array<[string, RegExp]> = [
|
|
642
|
+
['1.0', /^Invalid denylist JSON: number `1\.0` is not/],
|
|
643
|
+
['1e3', /^Invalid denylist JSON: number `1e3` is not/],
|
|
644
|
+
['1E3', /^Invalid denylist JSON: number `1E3` is not/],
|
|
645
|
+
['-1', /^Invalid denylist JSON: number `-1` is not/],
|
|
646
|
+
['-0', /^Invalid denylist JSON: number `-0` is not/],
|
|
647
|
+
['4294967296', /^Invalid denylist rule at index 0: match.originDomain: /],
|
|
648
|
+
[
|
|
649
|
+
'"4294967296"',
|
|
650
|
+
/^Invalid denylist rule at index 0: match.originDomain: /,
|
|
651
|
+
],
|
|
652
|
+
['"-0"', /^Invalid denylist rule at index 0: match.originDomain: /],
|
|
653
|
+
['"+"', /^Invalid denylist rule at index 0: match.originDomain: /],
|
|
654
|
+
['"1.0"', /^Invalid denylist rule at index 0: match.originDomain: /],
|
|
655
|
+
];
|
|
656
|
+
for (const [literal, message] of rejectedDomains) {
|
|
657
|
+
for (const value of [literal, `[${literal}]`]) {
|
|
658
|
+
it(`rejects the domain ${value}`, () => {
|
|
659
|
+
const error = captureLoadError(domainFile(value));
|
|
660
|
+
|
|
661
|
+
expect(error.failure).to.equal(DenylistLoadFailure.InvalidFile);
|
|
662
|
+
expect(error.message).to.match(message);
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
const acceptedBodyRegexes: Array<[string, string]> = [
|
|
668
|
+
['a named group', '(?<n>ab)'],
|
|
669
|
+
['a P-named group', '(?P<n>ab)'],
|
|
670
|
+
['a group name starting with _', '(?<_n>a)'],
|
|
671
|
+
['an escaped parenthesis before ?=', '\\(?=a'],
|
|
672
|
+
['(?= inside a class', '[(?=]'],
|
|
673
|
+
['a class with a trailing dash', '[a-]'],
|
|
674
|
+
['a class with a leading dash', '[-a]'],
|
|
675
|
+
['an escaped class before a trailing dash', '[\\d-]'],
|
|
676
|
+
['a POSIX class before a range dash', '[[:alpha:]-z]'],
|
|
677
|
+
['a POSIX class', '[[:alpha:]]'],
|
|
678
|
+
['a negated POSIX class', '[[:^digit:]]'],
|
|
679
|
+
['a class opened by ]', '[]a]'],
|
|
680
|
+
['an inline flag', '(?i)ab'],
|
|
681
|
+
['text anchors', '\\Aab\\z'],
|
|
682
|
+
['an escaped dash', '\\-'],
|
|
683
|
+
['an open-ended count', 'a{2,}'],
|
|
684
|
+
['a zero count', 'a{0}'],
|
|
685
|
+
['a count after a braced Unicode class', '\\p{L}{2}'],
|
|
686
|
+
['a count after a braced hex escape', '\\x{61}{2}'],
|
|
687
|
+
['groups nested 60 deep', `${'('.repeat(60)}a${')'.repeat(60)}`],
|
|
688
|
+
['a range across the surrogates', '[\\x{d000}-\\x{e000}]'],
|
|
689
|
+
['the code point before the surrogates', '\\x{d7ff}'],
|
|
690
|
+
['a word class repeated 100 times', '\\w{100}'],
|
|
691
|
+
['a hex class repeated 1000 times', '[0-9a-f]{1000}'],
|
|
692
|
+
['the mainnet recipient prefix', '^0{24}[0-9a-f]{40}'],
|
|
693
|
+
['a repeated group at the size estimate', '(?:\\w{50})+'],
|
|
694
|
+
['an open-ended count at the size estimate', '\\w{99,}'],
|
|
695
|
+
['alternatives at the size estimate', '\\w{50}|\\w{49}'],
|
|
696
|
+
['captures at the size estimate', '(a)'.repeat(5000)],
|
|
697
|
+
['non-capturing groups at the size estimate', '(?:a)'.repeat(10000)],
|
|
698
|
+
['optionals at the size estimate', '.?'.repeat(5000)],
|
|
699
|
+
['optional empty groups at the size estimate', '(?:)?'.repeat(10000)],
|
|
700
|
+
['zero counts within the size estimate', 'a{0}'.repeat(20000)],
|
|
701
|
+
['a lazy count at the size estimate', '\\w{100}?'],
|
|
702
|
+
];
|
|
703
|
+
for (const [syntax, bodyRegex] of acceptedBodyRegexes) {
|
|
704
|
+
it(`accepts a bodyRegex with ${syntax}`, () => {
|
|
705
|
+
const rules = parseDenylistFile(
|
|
706
|
+
rulesFile([
|
|
707
|
+
{
|
|
708
|
+
reason: 'r',
|
|
709
|
+
addedBy: 'a',
|
|
710
|
+
match: { originDomain: 1, bodyRegex },
|
|
711
|
+
},
|
|
712
|
+
]),
|
|
713
|
+
);
|
|
714
|
+
|
|
715
|
+
expect(rules.matchRules[0].bodyRegex?.pattern()).to.equal(bodyRegex);
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
it('rejects the whole file when an unrelated rule exceeds the relayer size limit', () => {
|
|
720
|
+
const error = captureLoadError(
|
|
721
|
+
rulesFile([
|
|
722
|
+
{
|
|
723
|
+
reason: 'r',
|
|
724
|
+
addedBy: 'a',
|
|
725
|
+
match: { originDomain: 999, bodyRegex: '\\D{900}' },
|
|
726
|
+
},
|
|
727
|
+
{ reason: 'r', addedBy: 'a', match: { messageId: MESSAGE_ID } },
|
|
728
|
+
]),
|
|
729
|
+
);
|
|
730
|
+
|
|
731
|
+
expect(error.failure).to.equal(DenylistLoadFailure.InvalidFile);
|
|
732
|
+
expect(error.message).to.match(
|
|
733
|
+
/may exceed the relayer's regex size limit/,
|
|
734
|
+
);
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
it('reports the index of the first invalid rule', () => {
|
|
738
|
+
const error = captureLoadError(
|
|
739
|
+
rulesFile([
|
|
740
|
+
{ reason: 'r', addedBy: 'a', match: { messageId: MESSAGE_ID } },
|
|
741
|
+
{ reason: 'r', addedBy: 'a', address: 'nope' },
|
|
742
|
+
]),
|
|
743
|
+
);
|
|
744
|
+
|
|
745
|
+
expect(error.failure).to.equal(DenylistLoadFailure.InvalidFile);
|
|
746
|
+
expect(error.message).to.match(/^Invalid denylist rule at index 1: /);
|
|
747
|
+
});
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
describe('findDenyReason', () => {
|
|
751
|
+
interface MatchCase {
|
|
752
|
+
name: string;
|
|
753
|
+
rule: Record<string, unknown>;
|
|
754
|
+
message: ExplorerMessage;
|
|
755
|
+
denied: boolean;
|
|
756
|
+
}
|
|
757
|
+
const cases: MatchCase[] = [
|
|
758
|
+
{
|
|
759
|
+
name: 'messageId rule matches its message',
|
|
760
|
+
rule: { match: { messageId: MESSAGE_ID } },
|
|
761
|
+
message: buildMessage({ msg_id: MESSAGE_ID }),
|
|
762
|
+
denied: true,
|
|
763
|
+
},
|
|
764
|
+
{
|
|
765
|
+
name: 'messageId rule matches case-insensitively',
|
|
766
|
+
rule: {
|
|
767
|
+
match: { messageId: MESSAGE_ID.toUpperCase().replace('0X', '0x') },
|
|
768
|
+
},
|
|
769
|
+
message: buildMessage({ msg_id: MESSAGE_ID }),
|
|
770
|
+
denied: true,
|
|
771
|
+
},
|
|
772
|
+
{
|
|
773
|
+
name: 'messageId rule ignores other messages',
|
|
774
|
+
rule: { match: { messageId: MESSAGE_ID } },
|
|
775
|
+
message: buildMessage({ msg_id: OTHER_MESSAGE_ID }),
|
|
776
|
+
denied: false,
|
|
777
|
+
},
|
|
778
|
+
{
|
|
779
|
+
name: 'domain and address rule matches when every field matches',
|
|
780
|
+
rule: {
|
|
781
|
+
match: {
|
|
782
|
+
originDomain: 42161,
|
|
783
|
+
senderAddress: [UNRELATED_ADDRESS, ORIGIN_ROUTER],
|
|
784
|
+
destinationDomain: '1',
|
|
785
|
+
recipientAddress: [DESTINATION_ROUTER],
|
|
786
|
+
},
|
|
787
|
+
},
|
|
788
|
+
message: buildMessage({ msg_id: MESSAGE_ID }),
|
|
789
|
+
denied: true,
|
|
790
|
+
},
|
|
791
|
+
{
|
|
792
|
+
name: 'domain and address rule ignores a different destination',
|
|
793
|
+
rule: {
|
|
794
|
+
match: {
|
|
795
|
+
originDomain: 42161,
|
|
796
|
+
senderAddress: [ORIGIN_ROUTER],
|
|
797
|
+
destinationDomain: 8453,
|
|
798
|
+
recipientAddress: [DESTINATION_ROUTER],
|
|
799
|
+
},
|
|
800
|
+
},
|
|
801
|
+
message: buildMessage({ msg_id: MESSAGE_ID }),
|
|
802
|
+
denied: false,
|
|
803
|
+
},
|
|
804
|
+
{
|
|
805
|
+
name: 'match keys are case-insensitive',
|
|
806
|
+
rule: { match: { DESTINATIONDOMAIN: [1], recipientaddress: '*' } },
|
|
807
|
+
message: buildMessage({ msg_id: MESSAGE_ID }),
|
|
808
|
+
denied: true,
|
|
809
|
+
},
|
|
810
|
+
{
|
|
811
|
+
name: 'address rule matches the message sender',
|
|
812
|
+
rule: { address: ORIGIN_ROUTER },
|
|
813
|
+
message: buildMessage({ msg_id: MESSAGE_ID }),
|
|
814
|
+
denied: true,
|
|
815
|
+
},
|
|
816
|
+
{
|
|
817
|
+
name: 'address rule matches the message recipient',
|
|
818
|
+
rule: { address: DESTINATION_ROUTER },
|
|
819
|
+
message: buildMessage({ msg_id: MESSAGE_ID }),
|
|
820
|
+
denied: true,
|
|
821
|
+
},
|
|
822
|
+
{
|
|
823
|
+
name: 'address rule matches the warp transfer recipient',
|
|
824
|
+
rule: { address: WARP_RECIPIENT },
|
|
825
|
+
message: buildMessage({ msg_id: MESSAGE_ID }),
|
|
826
|
+
denied: true,
|
|
827
|
+
},
|
|
828
|
+
{
|
|
829
|
+
name: 'address rule matches the origin tx sender',
|
|
830
|
+
rule: { address: TX_SENDER },
|
|
831
|
+
message: buildMessage({ msg_id: MESSAGE_ID }),
|
|
832
|
+
denied: true,
|
|
833
|
+
},
|
|
834
|
+
{
|
|
835
|
+
name: 'address rule matches a 32-byte Solana sender',
|
|
836
|
+
rule: { address: SOLANA_ADDRESS },
|
|
837
|
+
message: buildMessage({
|
|
838
|
+
msg_id: MESSAGE_ID,
|
|
839
|
+
sender:
|
|
840
|
+
'0x8fa155f74d49e7464faa0d27041225f2ec5b8a5632fc68efb022d57946ec19c9',
|
|
841
|
+
}),
|
|
842
|
+
denied: true,
|
|
843
|
+
},
|
|
844
|
+
{
|
|
845
|
+
name: 'address rule matches a Tron origin tx sender',
|
|
846
|
+
rule: { address: TRON_ADDRESS },
|
|
847
|
+
message: buildMessage({
|
|
848
|
+
msg_id: MESSAGE_ID,
|
|
849
|
+
origin_tx_sender: '0x430b9b4a60d18affe500b731a97b5aa622c7863f',
|
|
850
|
+
}),
|
|
851
|
+
denied: true,
|
|
852
|
+
},
|
|
853
|
+
{
|
|
854
|
+
name: 'address rule does not scan the rest of the body',
|
|
855
|
+
rule: { address: UNRELATED_ADDRESS },
|
|
856
|
+
message: buildMessage({
|
|
857
|
+
msg_id: MESSAGE_ID,
|
|
858
|
+
message_body: `0x${'00'.repeat(32)}${UNRELATED_ADDRESS.slice(2)}`,
|
|
859
|
+
}),
|
|
860
|
+
denied: false,
|
|
861
|
+
},
|
|
862
|
+
{
|
|
863
|
+
name: 'bodyRegex with mixed-case keys matches the hex body unanchored',
|
|
864
|
+
rule: {
|
|
865
|
+
match: {
|
|
866
|
+
MeSsAgEiD: MESSAGE_ID,
|
|
867
|
+
ORIGINDOMAIN: 42161,
|
|
868
|
+
DestinationDomain: 1,
|
|
869
|
+
BodyRegex: '^a\\D$',
|
|
870
|
+
},
|
|
871
|
+
},
|
|
872
|
+
message: buildMessage({ msg_id: MESSAGE_ID, message_body: '0xab' }),
|
|
873
|
+
denied: true,
|
|
874
|
+
},
|
|
875
|
+
{
|
|
876
|
+
name: 'bodyRegex rule ignores a body that does not match',
|
|
877
|
+
rule: { match: { originDomain: 42161, bodyRegex: '^a\\D$' } },
|
|
878
|
+
message: buildMessage({ msg_id: MESSAGE_ID, message_body: '0xa1' }),
|
|
879
|
+
denied: false,
|
|
880
|
+
},
|
|
881
|
+
{
|
|
882
|
+
name: 'bodyRegex must match together with the other fields',
|
|
883
|
+
rule: { match: { originDomain: 1, bodyRegex: 'ab' } },
|
|
884
|
+
message: buildMessage({ msg_id: MESSAGE_ID, message_body: '0xab' }),
|
|
885
|
+
denied: false,
|
|
886
|
+
},
|
|
887
|
+
{
|
|
888
|
+
name: 'bodyRegex matches a lowercase body without 0x',
|
|
889
|
+
rule: { match: { destinationDomain: 1, bodyRegex: '^ab$' } },
|
|
890
|
+
message: buildMessage({ msg_id: MESSAGE_ID, message_body: '0xAB' }),
|
|
891
|
+
denied: true,
|
|
892
|
+
},
|
|
893
|
+
{
|
|
894
|
+
name: 'bodyRegex matches a substring of the body',
|
|
895
|
+
rule: { match: { destinationDomain: 1, bodyRegex: 'bc' } },
|
|
896
|
+
message: buildMessage({ msg_id: MESSAGE_ID, message_body: '0xabcd' }),
|
|
897
|
+
denied: true,
|
|
898
|
+
},
|
|
899
|
+
{
|
|
900
|
+
name: 'a 20-byte zero match.senderAddress matches the zero sender',
|
|
901
|
+
rule: {
|
|
902
|
+
match: {
|
|
903
|
+
senderAddress: '0x0000000000000000000000000000000000000000',
|
|
904
|
+
},
|
|
905
|
+
},
|
|
906
|
+
message: buildMessage({
|
|
907
|
+
msg_id: MESSAGE_ID,
|
|
908
|
+
sender: ZERO_ADDRESS_HEX_32,
|
|
909
|
+
}),
|
|
910
|
+
denied: true,
|
|
911
|
+
},
|
|
912
|
+
{
|
|
913
|
+
name: 'a 20-byte zero match.recipientAddress matches the zero recipient',
|
|
914
|
+
rule: {
|
|
915
|
+
match: {
|
|
916
|
+
recipientAddress: '0x0000000000000000000000000000000000000000',
|
|
917
|
+
},
|
|
918
|
+
},
|
|
919
|
+
message: buildMessage({
|
|
920
|
+
msg_id: MESSAGE_ID,
|
|
921
|
+
recipient: ZERO_ADDRESS_HEX_32,
|
|
922
|
+
}),
|
|
923
|
+
denied: true,
|
|
924
|
+
},
|
|
925
|
+
{
|
|
926
|
+
name: 'a 20-byte zero match.messageId matches the zero message id',
|
|
927
|
+
rule: {
|
|
928
|
+
match: { messageId: '0x0000000000000000000000000000000000000000' },
|
|
929
|
+
},
|
|
930
|
+
message: buildMessage({ msg_id: ZERO_ADDRESS_HEX_32 }),
|
|
931
|
+
denied: true,
|
|
932
|
+
},
|
|
933
|
+
{
|
|
934
|
+
name: 'a null bodyRegex is no constraint',
|
|
935
|
+
rule: { match: { messageId: MESSAGE_ID, bodyRegex: null } },
|
|
936
|
+
message: buildMessage({ msg_id: MESSAGE_ID }),
|
|
937
|
+
denied: true,
|
|
938
|
+
},
|
|
939
|
+
{
|
|
940
|
+
name: 'bodyRegex honours an inline case-insensitive flag',
|
|
941
|
+
rule: { match: { destinationDomain: 1, bodyRegex: '(?i)^AB$' } },
|
|
942
|
+
message: buildMessage({ msg_id: MESSAGE_ID, message_body: '0xab' }),
|
|
943
|
+
denied: true,
|
|
944
|
+
},
|
|
945
|
+
{
|
|
946
|
+
name: 'bodyRegex matches between text anchors',
|
|
947
|
+
rule: { match: { destinationDomain: 1, bodyRegex: '\\Aab\\z' } },
|
|
948
|
+
message: buildMessage({ msg_id: MESSAGE_ID, message_body: '0xab' }),
|
|
949
|
+
denied: true,
|
|
950
|
+
},
|
|
951
|
+
{
|
|
952
|
+
name: 'bodyRegex text anchors reject a longer body',
|
|
953
|
+
rule: { match: { destinationDomain: 1, bodyRegex: '\\Aab\\z' } },
|
|
954
|
+
message: buildMessage({ msg_id: MESSAGE_ID, message_body: '0xabab' }),
|
|
955
|
+
denied: false,
|
|
956
|
+
},
|
|
957
|
+
{
|
|
958
|
+
name: 'bodyRegex matches through a P-named group',
|
|
959
|
+
rule: { match: { destinationDomain: 1, bodyRegex: '^(?P<n>ab)cd$' } },
|
|
960
|
+
message: buildMessage({ msg_id: MESSAGE_ID, message_body: '0xabcd' }),
|
|
961
|
+
denied: true,
|
|
962
|
+
},
|
|
963
|
+
{
|
|
964
|
+
name: 'the mainnet relayer bodyRegex matches its message',
|
|
965
|
+
rule: {
|
|
966
|
+
match: { destinationDomain: 1, bodyRegex: MAINNET_BODY_REGEX },
|
|
967
|
+
},
|
|
968
|
+
message: buildMessage({
|
|
969
|
+
msg_id: MESSAGE_ID,
|
|
970
|
+
message_body: `0x${MAINNET_BODY_PREFIX}${'00'.repeat(32)}`,
|
|
971
|
+
}),
|
|
972
|
+
denied: true,
|
|
973
|
+
},
|
|
974
|
+
{
|
|
975
|
+
name: 'the mainnet relayer bodyRegex ignores another message',
|
|
976
|
+
rule: {
|
|
977
|
+
match: { destinationDomain: 1, bodyRegex: MAINNET_BODY_REGEX },
|
|
978
|
+
},
|
|
979
|
+
message: buildMessage({
|
|
980
|
+
msg_id: MESSAGE_ID,
|
|
981
|
+
message_body: `0x02${MAINNET_BODY_PREFIX.slice(2)}${'00'.repeat(32)}`,
|
|
982
|
+
}),
|
|
983
|
+
denied: false,
|
|
984
|
+
},
|
|
985
|
+
{
|
|
986
|
+
name: 'matchBody rule finds the address anywhere in the body',
|
|
987
|
+
rule: { address: BODY_ADDRESS, matchBody: true },
|
|
988
|
+
message: buildMessage({
|
|
989
|
+
msg_id: MESSAGE_ID,
|
|
990
|
+
message_body: `0x${'00'.repeat(45)}${BODY_ADDRESS.slice(2).toLowerCase()}${'00'.repeat(3)}`,
|
|
991
|
+
}),
|
|
992
|
+
denied: true,
|
|
993
|
+
},
|
|
994
|
+
{
|
|
995
|
+
name: 'matchBody rule ignores the address off a byte boundary',
|
|
996
|
+
rule: { address: BODY_ADDRESS, matchBody: true },
|
|
997
|
+
message: buildMessage({
|
|
998
|
+
msg_id: MESSAGE_ID,
|
|
999
|
+
message_body: `0x${'00'.repeat(45)}0${BODY_ADDRESS.slice(2).toLowerCase()}0${'00'.repeat(3)}`,
|
|
1000
|
+
}),
|
|
1001
|
+
denied: false,
|
|
1002
|
+
},
|
|
1003
|
+
{
|
|
1004
|
+
name: 'matchBody false does not scan the body',
|
|
1005
|
+
rule: { address: BODY_ADDRESS, matchBody: false },
|
|
1006
|
+
message: buildMessage({
|
|
1007
|
+
msg_id: MESSAGE_ID,
|
|
1008
|
+
message_body: `0x${'00'.repeat(45)}${BODY_ADDRESS.slice(2).toLowerCase()}${'00'.repeat(3)}`,
|
|
1009
|
+
}),
|
|
1010
|
+
denied: false,
|
|
1011
|
+
},
|
|
1012
|
+
{
|
|
1013
|
+
name: 'expired rule is ignored',
|
|
1014
|
+
rule: {
|
|
1015
|
+
expiresAt: '2026-11-03T23:59:59Z',
|
|
1016
|
+
match: { messageId: MESSAGE_ID },
|
|
1017
|
+
},
|
|
1018
|
+
message: buildMessage({ msg_id: MESSAGE_ID }),
|
|
1019
|
+
denied: false,
|
|
1020
|
+
},
|
|
1021
|
+
{
|
|
1022
|
+
name: 'rule expiring exactly now is ignored',
|
|
1023
|
+
rule: { expiresAt: '2026-11-04T00:00:00Z', address: TX_SENDER },
|
|
1024
|
+
message: buildMessage({ msg_id: MESSAGE_ID }),
|
|
1025
|
+
denied: false,
|
|
1026
|
+
},
|
|
1027
|
+
{
|
|
1028
|
+
name: 'rule expiring later is active',
|
|
1029
|
+
rule: {
|
|
1030
|
+
expiresAt: '2026-11-05T00:00:00+01:00',
|
|
1031
|
+
match: { messageId: MESSAGE_ID },
|
|
1032
|
+
},
|
|
1033
|
+
message: buildMessage({ msg_id: MESSAGE_ID }),
|
|
1034
|
+
denied: true,
|
|
1035
|
+
},
|
|
1036
|
+
];
|
|
1037
|
+
for (const c of cases) {
|
|
1038
|
+
it(c.name, () => {
|
|
1039
|
+
const rules = parseDenylistFile(
|
|
1040
|
+
rulesFile([
|
|
1041
|
+
{
|
|
1042
|
+
reason: BASE_RULE.reason,
|
|
1043
|
+
addedBy: BASE_RULE.addedBy,
|
|
1044
|
+
expiresAt: c.rule.expiresAt,
|
|
1045
|
+
match: c.rule.match,
|
|
1046
|
+
address: c.rule.address,
|
|
1047
|
+
matchBody: c.rule.matchBody,
|
|
1048
|
+
},
|
|
1049
|
+
]),
|
|
1050
|
+
);
|
|
1051
|
+
|
|
1052
|
+
const reason = findDenyReason(rules, c.message, NOW);
|
|
1053
|
+
|
|
1054
|
+
expect(reason).to.equal(
|
|
1055
|
+
c.denied ? 'test (added by tester)' : undefined,
|
|
1056
|
+
);
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
it('matches a bodyRegex in linear time', () => {
|
|
1061
|
+
const rules = parseDenylistFile(
|
|
1062
|
+
rulesFile([
|
|
1063
|
+
{
|
|
1064
|
+
reason: BASE_RULE.reason,
|
|
1065
|
+
addedBy: BASE_RULE.addedBy,
|
|
1066
|
+
match: { destinationDomain: 1, bodyRegex: '(0+)+1$' },
|
|
1067
|
+
},
|
|
1068
|
+
]),
|
|
1069
|
+
);
|
|
1070
|
+
const message = buildMessage({
|
|
1071
|
+
msg_id: MESSAGE_ID,
|
|
1072
|
+
message_body: `0x${'00'.repeat(64)}`,
|
|
1073
|
+
});
|
|
1074
|
+
|
|
1075
|
+
const start = performance.now();
|
|
1076
|
+
const reason = findDenyReason(rules, message, NOW);
|
|
1077
|
+
|
|
1078
|
+
expect(performance.now() - start).to.be.lessThan(1000);
|
|
1079
|
+
expect(reason).to.equal(undefined);
|
|
1080
|
+
});
|
|
1081
|
+
});
|
|
1082
|
+
|
|
1083
|
+
describe('findMessageIdDenyReason', () => {
|
|
1084
|
+
interface MessageIdCase {
|
|
1085
|
+
name: string;
|
|
1086
|
+
rule: Record<string, unknown>;
|
|
1087
|
+
messageId: string;
|
|
1088
|
+
denied: boolean;
|
|
1089
|
+
}
|
|
1090
|
+
const cases: MessageIdCase[] = [
|
|
1091
|
+
{
|
|
1092
|
+
name: 'messageId rule matches its message',
|
|
1093
|
+
rule: { match: { messageId: MESSAGE_ID } },
|
|
1094
|
+
messageId: MESSAGE_ID,
|
|
1095
|
+
denied: true,
|
|
1096
|
+
},
|
|
1097
|
+
{
|
|
1098
|
+
name: 'messageId rule matches an uppercase id',
|
|
1099
|
+
rule: { match: { messageId: MESSAGE_ID } },
|
|
1100
|
+
messageId: MESSAGE_ID.toUpperCase().replace('0X', '0x'),
|
|
1101
|
+
denied: true,
|
|
1102
|
+
},
|
|
1103
|
+
{
|
|
1104
|
+
name: 'uppercase messageId rule matches a lowercase id',
|
|
1105
|
+
rule: {
|
|
1106
|
+
match: { messageId: MESSAGE_ID.toUpperCase().replace('0X', '0x') },
|
|
1107
|
+
},
|
|
1108
|
+
messageId: MESSAGE_ID,
|
|
1109
|
+
denied: true,
|
|
1110
|
+
},
|
|
1111
|
+
{
|
|
1112
|
+
name: 'messageId rule ignores other messages',
|
|
1113
|
+
rule: { match: { messageId: MESSAGE_ID } },
|
|
1114
|
+
messageId: OTHER_MESSAGE_ID,
|
|
1115
|
+
denied: false,
|
|
1116
|
+
},
|
|
1117
|
+
{
|
|
1118
|
+
name: 'unexpired messageId rule matches',
|
|
1119
|
+
rule: {
|
|
1120
|
+
expiresAt: '2026-11-05T00:00:00Z',
|
|
1121
|
+
match: { messageId: MESSAGE_ID },
|
|
1122
|
+
},
|
|
1123
|
+
messageId: MESSAGE_ID,
|
|
1124
|
+
denied: true,
|
|
1125
|
+
},
|
|
1126
|
+
{
|
|
1127
|
+
name: 'expired messageId rule does not match',
|
|
1128
|
+
rule: {
|
|
1129
|
+
expiresAt: '2026-11-03T00:00:00Z',
|
|
1130
|
+
match: { messageId: MESSAGE_ID },
|
|
1131
|
+
},
|
|
1132
|
+
messageId: MESSAGE_ID,
|
|
1133
|
+
denied: false,
|
|
1134
|
+
},
|
|
1135
|
+
{
|
|
1136
|
+
name: 'messageId rule with an originDomain does not apply',
|
|
1137
|
+
rule: { match: { messageId: MESSAGE_ID, originDomain: 42161 } },
|
|
1138
|
+
messageId: MESSAGE_ID,
|
|
1139
|
+
denied: false,
|
|
1140
|
+
},
|
|
1141
|
+
{
|
|
1142
|
+
name: 'messageId rule with a senderAddress does not apply',
|
|
1143
|
+
rule: {
|
|
1144
|
+
match: { messageId: MESSAGE_ID, senderAddress: ORIGIN_ROUTER },
|
|
1145
|
+
},
|
|
1146
|
+
messageId: MESSAGE_ID,
|
|
1147
|
+
denied: false,
|
|
1148
|
+
},
|
|
1149
|
+
{
|
|
1150
|
+
name: 'messageId rule with a destinationDomain does not apply',
|
|
1151
|
+
rule: { match: { messageId: MESSAGE_ID, destinationDomain: 1 } },
|
|
1152
|
+
messageId: MESSAGE_ID,
|
|
1153
|
+
denied: false,
|
|
1154
|
+
},
|
|
1155
|
+
{
|
|
1156
|
+
name: 'messageId rule with a recipientAddress does not apply',
|
|
1157
|
+
rule: {
|
|
1158
|
+
match: {
|
|
1159
|
+
messageId: MESSAGE_ID,
|
|
1160
|
+
recipientAddress: DESTINATION_ROUTER,
|
|
1161
|
+
},
|
|
1162
|
+
},
|
|
1163
|
+
messageId: MESSAGE_ID,
|
|
1164
|
+
denied: false,
|
|
1165
|
+
},
|
|
1166
|
+
{
|
|
1167
|
+
name: 'messageId rule with a bodyRegex does not apply',
|
|
1168
|
+
rule: { match: { messageId: MESSAGE_ID, bodyRegex: '.*' } },
|
|
1169
|
+
messageId: MESSAGE_ID,
|
|
1170
|
+
denied: false,
|
|
1171
|
+
},
|
|
1172
|
+
{
|
|
1173
|
+
name: 'destinationDomain rule does not apply',
|
|
1174
|
+
rule: { match: { destinationDomain: 1 } },
|
|
1175
|
+
messageId: MESSAGE_ID,
|
|
1176
|
+
denied: false,
|
|
1177
|
+
},
|
|
1178
|
+
{
|
|
1179
|
+
name: 'route rule does not apply',
|
|
1180
|
+
rule: {
|
|
1181
|
+
match: {
|
|
1182
|
+
originDomain: 42161,
|
|
1183
|
+
senderAddress: ORIGIN_ROUTER,
|
|
1184
|
+
destinationDomain: 1,
|
|
1185
|
+
recipientAddress: DESTINATION_ROUTER,
|
|
1186
|
+
},
|
|
1187
|
+
},
|
|
1188
|
+
messageId: MESSAGE_ID,
|
|
1189
|
+
denied: false,
|
|
1190
|
+
},
|
|
1191
|
+
{
|
|
1192
|
+
name: 'address rule does not apply',
|
|
1193
|
+
rule: { address: MESSAGE_ID },
|
|
1194
|
+
messageId: MESSAGE_ID,
|
|
1195
|
+
denied: false,
|
|
1196
|
+
},
|
|
1197
|
+
];
|
|
1198
|
+
for (const c of cases) {
|
|
1199
|
+
it(c.name, () => {
|
|
1200
|
+
const rules = parseDenylistFile(
|
|
1201
|
+
rulesFile([
|
|
1202
|
+
{
|
|
1203
|
+
reason: BASE_RULE.reason,
|
|
1204
|
+
addedBy: BASE_RULE.addedBy,
|
|
1205
|
+
expiresAt: c.rule.expiresAt,
|
|
1206
|
+
match: c.rule.match,
|
|
1207
|
+
address: c.rule.address,
|
|
1208
|
+
},
|
|
1209
|
+
]),
|
|
1210
|
+
);
|
|
1211
|
+
|
|
1212
|
+
const reason = findMessageIdDenyReason(rules, c.messageId, NOW);
|
|
1213
|
+
|
|
1214
|
+
expect(reason).to.equal(
|
|
1215
|
+
c.denied ? 'test (added by tester)' : undefined,
|
|
1216
|
+
);
|
|
1217
|
+
});
|
|
1218
|
+
}
|
|
1219
|
+
});
|
|
1220
|
+
});
|