@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,934 @@
|
|
|
1
|
+
import bs58 from 'bs58';
|
|
2
|
+
import { ethers } from 'ethers';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { RE2JS } from 're2js';
|
|
5
|
+
import { bytesToHex } from 'viem';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
addressToBytes32,
|
|
10
|
+
addressToBytesCosmos,
|
|
11
|
+
addressToBytesRadix,
|
|
12
|
+
assert,
|
|
13
|
+
isNullish,
|
|
14
|
+
isValidAddressCosmos,
|
|
15
|
+
padBytesToLength,
|
|
16
|
+
strip0x,
|
|
17
|
+
ZERO_ADDRESS_HEX_32,
|
|
18
|
+
} from '@humanode-hyperlane/utils';
|
|
19
|
+
|
|
20
|
+
import type { ExplorerMessage } from '../../utils/ExplorerClient.js';
|
|
21
|
+
|
|
22
|
+
import { bodyContainsAddress, parseMatchBodyAddress } from './matchBody.js';
|
|
23
|
+
import { normalizeMessageId } from './types.js';
|
|
24
|
+
|
|
25
|
+
// Mirrors the relayer's runtime denylist parser
|
|
26
|
+
// (rust/main/agents/relayer/src/msg/runtime_denylist.rs): one invalid rule
|
|
27
|
+
// rejects the whole file.
|
|
28
|
+
|
|
29
|
+
export const DenylistLoadFailure = {
|
|
30
|
+
FetchFailed: 'fetch_failed',
|
|
31
|
+
InvalidFile: 'invalid_file',
|
|
32
|
+
} as const;
|
|
33
|
+
export type DenylistLoadFailure =
|
|
34
|
+
(typeof DenylistLoadFailure)[keyof typeof DenylistLoadFailure];
|
|
35
|
+
|
|
36
|
+
export class DenylistLoadError extends Error {
|
|
37
|
+
constructor(
|
|
38
|
+
readonly failure: DenylistLoadFailure,
|
|
39
|
+
message: string,
|
|
40
|
+
) {
|
|
41
|
+
super(message);
|
|
42
|
+
this.name = 'DenylistLoadError';
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Unset fields are wildcards. Addresses and ids are lowercase bytes32 hex. */
|
|
47
|
+
export interface DenylistMatchRule {
|
|
48
|
+
reason: string;
|
|
49
|
+
expiresAt?: number;
|
|
50
|
+
messageIds?: ReadonlySet<string>;
|
|
51
|
+
originDomains?: ReadonlySet<number>;
|
|
52
|
+
senders?: ReadonlySet<string>;
|
|
53
|
+
destinationDomains?: ReadonlySet<number>;
|
|
54
|
+
recipients?: ReadonlySet<string>;
|
|
55
|
+
/** Tested, unanchored, against the lowercase hex body without 0x. */
|
|
56
|
+
bodyRegex?: RE2JS;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface DenylistAddressRule {
|
|
60
|
+
reason: string;
|
|
61
|
+
expiresAt?: number;
|
|
62
|
+
address: string;
|
|
63
|
+
/** Set by `matchBody: true`: lowercase 20-byte hex searched in the body. */
|
|
64
|
+
bodyAddress?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface DenylistRules {
|
|
68
|
+
matchRules: DenylistMatchRule[];
|
|
69
|
+
addressRules: DenylistAddressRule[];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const MATCH_KEYS = {
|
|
73
|
+
messageId: 'messageid',
|
|
74
|
+
originDomain: 'origindomain',
|
|
75
|
+
senderAddress: 'senderaddress',
|
|
76
|
+
destinationDomain: 'destinationdomain',
|
|
77
|
+
recipientAddress: 'recipientaddress',
|
|
78
|
+
bodyRegex: 'bodyregex',
|
|
79
|
+
} as const;
|
|
80
|
+
|
|
81
|
+
const WILDCARD = '*';
|
|
82
|
+
const U32_MAX = 2 ** 32 - 1;
|
|
83
|
+
const TRON_ADDRESS_PREFIX = 0x41;
|
|
84
|
+
const TRON_ADDRESS_LENGTH = 25;
|
|
85
|
+
const BECH32_DATA_REGEX = /^[!-~]+1[02-9ac-hj-np-z]{6,}$/i;
|
|
86
|
+
|
|
87
|
+
// The grammar chrono's `parse_from_rfc3339` accepts, which the relayer uses.
|
|
88
|
+
const RFC3339_REGEX =
|
|
89
|
+
/^(\d{4})-(\d{2})-(\d{2})[Tt ](\d{2}):(\d{2}):(\d{2})(\.\d+)?(?:[Zz]|([+-])(\d{2}):(\d{2}))$/;
|
|
90
|
+
|
|
91
|
+
function daysInMonth(year: number, month: number): number {
|
|
92
|
+
return new Date(Date.UTC(year, month, 0)).getUTCDate();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Returns epoch milliseconds, or undefined if not an RFC 3339 timestamp. */
|
|
96
|
+
export function parseRfc3339(value: string): number | undefined {
|
|
97
|
+
const match = RFC3339_REGEX.exec(value);
|
|
98
|
+
if (!match) return undefined;
|
|
99
|
+
|
|
100
|
+
const [
|
|
101
|
+
,
|
|
102
|
+
year,
|
|
103
|
+
month,
|
|
104
|
+
day,
|
|
105
|
+
hour,
|
|
106
|
+
minute,
|
|
107
|
+
second,
|
|
108
|
+
fraction,
|
|
109
|
+
sign,
|
|
110
|
+
offsetHour,
|
|
111
|
+
offsetMinute,
|
|
112
|
+
] = match;
|
|
113
|
+
|
|
114
|
+
const fields = {
|
|
115
|
+
year: Number(year),
|
|
116
|
+
month: Number(month),
|
|
117
|
+
day: Number(day),
|
|
118
|
+
hour: Number(hour),
|
|
119
|
+
minute: Number(minute),
|
|
120
|
+
second: Number(second),
|
|
121
|
+
offsetHour: Number(offsetHour ?? 0),
|
|
122
|
+
offsetMinute: Number(offsetMinute ?? 0),
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
if (
|
|
126
|
+
fields.month < 1 ||
|
|
127
|
+
fields.month > 12 ||
|
|
128
|
+
fields.day < 1 ||
|
|
129
|
+
fields.day > daysInMonth(fields.year, fields.month) ||
|
|
130
|
+
fields.hour > 23 ||
|
|
131
|
+
fields.minute > 59 ||
|
|
132
|
+
fields.second > 60 ||
|
|
133
|
+
fields.offsetHour > 23 ||
|
|
134
|
+
fields.offsetMinute > 59
|
|
135
|
+
) {
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const offsetMs =
|
|
140
|
+
(sign === '-' ? -1 : 1) *
|
|
141
|
+
(fields.offsetHour * 60 + fields.offsetMinute) *
|
|
142
|
+
60_000;
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
Date.UTC(
|
|
146
|
+
fields.year,
|
|
147
|
+
fields.month - 1,
|
|
148
|
+
fields.day,
|
|
149
|
+
fields.hour,
|
|
150
|
+
fields.minute,
|
|
151
|
+
fields.second,
|
|
152
|
+
) +
|
|
153
|
+
Math.floor(Number(`0${fraction ?? ''}`) * 1_000) -
|
|
154
|
+
offsetMs
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const nonEmptyString = (field: string) =>
|
|
159
|
+
z.string().refine((value) => value.trim().length > 0, {
|
|
160
|
+
message: `\`${field}\` must not be empty`,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const DenylistFileSchema = z.strictObject({ rules: z.array(z.unknown()) });
|
|
164
|
+
|
|
165
|
+
const DenylistRuleSchema = z
|
|
166
|
+
.strictObject({
|
|
167
|
+
reason: nonEmptyString('reason'),
|
|
168
|
+
addedBy: nonEmptyString('addedBy'),
|
|
169
|
+
expiresAt: z
|
|
170
|
+
.string()
|
|
171
|
+
.refine((value) => parseRfc3339(value) !== undefined, {
|
|
172
|
+
message: '`expiresAt` must be an RFC 3339 timestamp',
|
|
173
|
+
})
|
|
174
|
+
.nullish(),
|
|
175
|
+
match: z.record(z.string(), z.unknown()).nullish(),
|
|
176
|
+
address: z.string().nullish(),
|
|
177
|
+
matchBody: z.boolean().optional(),
|
|
178
|
+
})
|
|
179
|
+
.refine((rule) => isNullish(rule.match) !== isNullish(rule.address), {
|
|
180
|
+
message: 'Exactly one of `match` or `address` must be set',
|
|
181
|
+
})
|
|
182
|
+
.refine((rule) => rule.matchBody === undefined || !isNullish(rule.address), {
|
|
183
|
+
message: '`matchBody` is only valid on an `address` rule',
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// Rust's `str::parse::<u32>` accepts a leading `+` and leading zeros.
|
|
187
|
+
const DomainValueSchema = z.union([
|
|
188
|
+
z.number().int().nonnegative().max(U32_MAX),
|
|
189
|
+
z
|
|
190
|
+
.string()
|
|
191
|
+
.regex(/^\+?\d+$/, 'domain must be a decimal integer')
|
|
192
|
+
.transform(Number)
|
|
193
|
+
.pipe(z.number().max(U32_MAX)),
|
|
194
|
+
]);
|
|
195
|
+
const DomainFilterSchema = z.union([
|
|
196
|
+
z.literal(WILDCARD),
|
|
197
|
+
DomainValueSchema.transform((value) => [value]),
|
|
198
|
+
z.array(DomainValueSchema),
|
|
199
|
+
]);
|
|
200
|
+
const AddressFilterSchema = z.union([
|
|
201
|
+
z.literal(WILDCARD),
|
|
202
|
+
z.string().transform((value) => [value]),
|
|
203
|
+
z.array(z.string()),
|
|
204
|
+
]);
|
|
205
|
+
|
|
206
|
+
function sha256(bytes: Uint8Array): Buffer {
|
|
207
|
+
return createHash('sha256').update(bytes).digest();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function verifyTronChecksum(value: string, bytes: Uint8Array): void {
|
|
211
|
+
const payload = bytes.subarray(0, 21);
|
|
212
|
+
const checksum = sha256(sha256(payload)).subarray(0, 4);
|
|
213
|
+
|
|
214
|
+
if (
|
|
215
|
+
bytes[0] !== TRON_ADDRESS_PREFIX ||
|
|
216
|
+
!checksum.equals(bytes.subarray(21))
|
|
217
|
+
) {
|
|
218
|
+
throw new Error(
|
|
219
|
+
`Tron address \`${value}\` failed its prefix or checksum check`,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function isBech32Like(value: string): boolean {
|
|
225
|
+
return (
|
|
226
|
+
(value === value.toLowerCase() || value === value.toUpperCase()) &&
|
|
227
|
+
BECH32_DATA_REGEX.test(value)
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function decodeBech32(value: string): Uint8Array | undefined {
|
|
232
|
+
if (isValidAddressCosmos(value)) return addressToBytesCosmos(value);
|
|
233
|
+
|
|
234
|
+
// bech32m (e.g. Radix) has no validator that accepts any prefix.
|
|
235
|
+
try {
|
|
236
|
+
return addressToBytesRadix(value);
|
|
237
|
+
} catch {
|
|
238
|
+
// Not bech32m either; the caller falls back to base58 like the relayer.
|
|
239
|
+
return undefined;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Parses an address the way the relayer's `hex_or_base58_or_bech32_to_h256`
|
|
245
|
+
* does, plus its Tron checksum check, into lowercase bytes32 hex.
|
|
246
|
+
*/
|
|
247
|
+
export function parseDenylistAddress(value: string): string {
|
|
248
|
+
if (value.startsWith('0x')) {
|
|
249
|
+
if (
|
|
250
|
+
ethers.utils.isHexString(value, 20) ||
|
|
251
|
+
ethers.utils.isHexString(value, 32)
|
|
252
|
+
) {
|
|
253
|
+
return bytesToHex(
|
|
254
|
+
padBytesToLength(Buffer.from(strip0x(value), 'hex'), 32),
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
throw new Error(`Invalid hex address \`${value}\``);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const bech32Bytes = isBech32Like(value) ? decodeBech32(value) : undefined;
|
|
262
|
+
if (bech32Bytes) return bytesToHex(padBytesToLength(bech32Bytes, 32));
|
|
263
|
+
|
|
264
|
+
const bytes = bs58.decodeUnsafe(value);
|
|
265
|
+
if (!bytes) throw new Error(`Invalid address \`${value}\``);
|
|
266
|
+
|
|
267
|
+
if (bytes.length === TRON_ADDRESS_LENGTH) {
|
|
268
|
+
verifyTronChecksum(value, bytes);
|
|
269
|
+
return bytesToHex(padBytesToLength(bytes.subarray(1, 21), 32));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (bytes.length !== 32) {
|
|
273
|
+
throw new Error(`Invalid length of base58 address \`${value}\``);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return bytesToHex(padBytesToLength(bytes, 32));
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function formatZodError(error: z.ZodError): string {
|
|
280
|
+
return error.issues
|
|
281
|
+
.map((issue) =>
|
|
282
|
+
issue.path.length > 0
|
|
283
|
+
? `${issue.path.join('.')}: ${issue.message}`
|
|
284
|
+
: issue.message,
|
|
285
|
+
)
|
|
286
|
+
.join('; ');
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function parseOrThrow<T extends z.ZodType>(
|
|
290
|
+
schema: T,
|
|
291
|
+
value: unknown,
|
|
292
|
+
field: string,
|
|
293
|
+
): z.output<T> {
|
|
294
|
+
const result = schema.safeParse(value);
|
|
295
|
+
if (!result.success) {
|
|
296
|
+
throw new DenylistLoadError(
|
|
297
|
+
DenylistLoadFailure.InvalidFile,
|
|
298
|
+
`${field}: ${formatZodError(result.error)}`,
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return result.data;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function parseAddressFilter(
|
|
306
|
+
value: unknown,
|
|
307
|
+
field: string,
|
|
308
|
+
): ReadonlySet<string> | undefined {
|
|
309
|
+
const parsed = parseOrThrow(AddressFilterSchema, value, field);
|
|
310
|
+
if (parsed === WILDCARD) return undefined;
|
|
311
|
+
|
|
312
|
+
if (parsed.length === 0) {
|
|
313
|
+
throw new DenylistLoadError(
|
|
314
|
+
DenylistLoadFailure.InvalidFile,
|
|
315
|
+
`${field} is an empty list, so it would block nothing`,
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return new Set(parsed.map((address) => parseDenylistAddress(address)));
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function parseDomainFilter(
|
|
323
|
+
value: unknown,
|
|
324
|
+
field: string,
|
|
325
|
+
): ReadonlySet<number> | undefined {
|
|
326
|
+
const parsed = parseOrThrow(DomainFilterSchema, value, field);
|
|
327
|
+
if (parsed === WILDCARD) return undefined;
|
|
328
|
+
|
|
329
|
+
if (parsed.length === 0) {
|
|
330
|
+
throw new DenylistLoadError(
|
|
331
|
+
DenylistLoadFailure.InvalidFile,
|
|
332
|
+
`${field} is an empty list, so it would block nothing`,
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return new Set(parsed);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const BodyRegexSchema = z.string().nullish();
|
|
340
|
+
|
|
341
|
+
const COUNTED_REPETITION_REGEX = /^\{(0|[1-9][0-9]*)(,(0|[1-9][0-9]*)?)?\}/;
|
|
342
|
+
const POSIX_CLASS_REGEX = /^\[:\^?[a-z]+:\]/;
|
|
343
|
+
const NAMED_GROUP_OPENER_REGEX = /^\(\?(P<|<(?![=!]))/;
|
|
344
|
+
const FLAG_GROUP_REGEX = /^\(\?[A-Za-z-]*\)/;
|
|
345
|
+
const DIVERGENT_CLASS_SEQUENCES = ['&&', '--', '~~', '-['];
|
|
346
|
+
const MAX_GROUP_DEPTH = 60;
|
|
347
|
+
const NEGATED_PROPERTY_REGEX = /^\\[pP]\{\^/;
|
|
348
|
+
|
|
349
|
+
/** The code point of a `\x{...}` escape. */
|
|
350
|
+
function bracedHexValue(escape: string): number | undefined {
|
|
351
|
+
const hex = /^\\x\{([0-9A-Fa-f]+)\}/.exec(escape)?.[1];
|
|
352
|
+
|
|
353
|
+
return hex === undefined ? undefined : parseInt(hex, 16);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** Surrogates are not Unicode scalar values, which Rust requires. */
|
|
357
|
+
function isSurrogateEscape(escape: string): boolean {
|
|
358
|
+
const codePoint = bracedHexValue(escape);
|
|
359
|
+
|
|
360
|
+
return codePoint !== undefined && codePoint >= 0xd800 && codePoint <= 0xdfff;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function escapeEnd(pattern: string, index: number): number {
|
|
364
|
+
const next = pattern[index + 1] ?? '';
|
|
365
|
+
if (/[pPx]/.test(next) && pattern[index + 2] === '{') {
|
|
366
|
+
const close = pattern.indexOf('}', index + 3);
|
|
367
|
+
return close === -1 ? pattern.length : close + 1;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return /[pP]/.test(next) ? index + 3 : index + 2;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Whether a pattern is one RE2 accepts and the relayer's Rust `regex` crate
|
|
375
|
+
* rejects or reads differently. See {@link parseBodyRegex}.
|
|
376
|
+
*/
|
|
377
|
+
function divergesFromRelayerRegex(pattern: string): boolean {
|
|
378
|
+
let inClass = false;
|
|
379
|
+
let groupDepth = 0;
|
|
380
|
+
let index = 0;
|
|
381
|
+
while (index < pattern.length) {
|
|
382
|
+
const char = pattern[index];
|
|
383
|
+
|
|
384
|
+
if (char === '\\') {
|
|
385
|
+
const next = pattern[index + 1] ?? '';
|
|
386
|
+
if (/[0-9Q<>]/.test(next)) return true;
|
|
387
|
+
if (!inClass && /[bB]/.test(next) && pattern[index + 2] === '{') {
|
|
388
|
+
return true;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const end = escapeEnd(pattern, index);
|
|
392
|
+
const escape = pattern.slice(index, end);
|
|
393
|
+
if (NEGATED_PROPERTY_REGEX.test(escape) || isSurrogateEscape(escape)) {
|
|
394
|
+
return true;
|
|
395
|
+
}
|
|
396
|
+
if (
|
|
397
|
+
inClass &&
|
|
398
|
+
/[dDwWsSpP]/.test(next) &&
|
|
399
|
+
pattern[end] === '-' &&
|
|
400
|
+
pattern[end + 1] !== ']'
|
|
401
|
+
) {
|
|
402
|
+
return true;
|
|
403
|
+
}
|
|
404
|
+
index = end;
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (inClass) {
|
|
409
|
+
if (char === ']') {
|
|
410
|
+
inClass = false;
|
|
411
|
+
index++;
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
if (char === '[') {
|
|
415
|
+
const posixClass = POSIX_CLASS_REGEX.exec(pattern.slice(index));
|
|
416
|
+
if (!posixClass) return true;
|
|
417
|
+
index += posixClass[0].length;
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
if (
|
|
421
|
+
DIVERGENT_CLASS_SEQUENCES.some((sequence) =>
|
|
422
|
+
pattern.startsWith(sequence, index),
|
|
423
|
+
)
|
|
424
|
+
) {
|
|
425
|
+
return true;
|
|
426
|
+
}
|
|
427
|
+
index++;
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (char === '[') {
|
|
432
|
+
inClass = true;
|
|
433
|
+
index++;
|
|
434
|
+
if (pattern[index] === '^') index++;
|
|
435
|
+
if (pattern[index] === ']') index++;
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (char === '{') {
|
|
440
|
+
const repetition = COUNTED_REPETITION_REGEX.exec(pattern.slice(index));
|
|
441
|
+
if (!repetition) return true;
|
|
442
|
+
index += repetition[0].length;
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (char === '(') {
|
|
447
|
+
const rest = pattern.slice(index);
|
|
448
|
+
const flagGroup = FLAG_GROUP_REGEX.exec(rest);
|
|
449
|
+
if (
|
|
450
|
+
flagGroup &&
|
|
451
|
+
/[*+?{]/.test(pattern[index + flagGroup[0].length] ?? '')
|
|
452
|
+
) {
|
|
453
|
+
return true;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const namedGroup = NAMED_GROUP_OPENER_REGEX.exec(rest);
|
|
457
|
+
if (
|
|
458
|
+
namedGroup &&
|
|
459
|
+
!/[A-Za-z_]/.test(pattern[index + namedGroup[0].length] ?? '')
|
|
460
|
+
) {
|
|
461
|
+
return true;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
groupDepth++;
|
|
465
|
+
if (groupDepth > MAX_GROUP_DEPTH) return true;
|
|
466
|
+
} else if (char === ')') {
|
|
467
|
+
groupDepth--;
|
|
468
|
+
}
|
|
469
|
+
index++;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return false;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/** Rust's compiled-size limit, in units of the cheapest atom. */
|
|
476
|
+
const MAX_RELAYER_REGEX_SIZE = 10_000;
|
|
477
|
+
|
|
478
|
+
interface ScannedAtom {
|
|
479
|
+
end: number;
|
|
480
|
+
size: number;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Size units of an escape, measured against Rust `regex` 1.10.6. `\w`, `\W`,
|
|
485
|
+
* `\D` and Unicode properties cost 100, allowing 100 copies where Rust fails
|
|
486
|
+
* from about 200 (`\w`) to 3,000. `\d` and `\S` cost 10, allowing 1,000
|
|
487
|
+
* where Rust fails from about 2,000 and 5,500. Other escapes cost 1 like a
|
|
488
|
+
* literal, allowing 10,000 where the cheapest atoms, like `.`, fail from
|
|
489
|
+
* about 11,000, a margin of only 1.1x.
|
|
490
|
+
*/
|
|
491
|
+
function escapeSize(escape: string): number {
|
|
492
|
+
const kind = escape[1] ?? '';
|
|
493
|
+
if (/[wWDpP]/.test(kind)) return 100;
|
|
494
|
+
if (/[dS]/.test(kind)) return 10;
|
|
495
|
+
|
|
496
|
+
return 1;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function isNonAsciiEscape(escape: string): boolean {
|
|
500
|
+
const codePoint = bracedHexValue(escape);
|
|
501
|
+
|
|
502
|
+
return codePoint !== undefined && codePoint > 0x7f;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* A class costs the sum of its members. An ASCII class costs at least 2, and
|
|
507
|
+
* Rust accepts at least 10,000 copies of one made of literals, ranges and
|
|
508
|
+
* POSIX classes. A class that is negated, or holds a negated POSIX class or a
|
|
509
|
+
* non-ASCII member, costs at least 100, as its complement or case folding can
|
|
510
|
+
* be costly: `(?i)[^\x{1e00}-\x{a7ff}]` fails from 3,000 copies and a negated
|
|
511
|
+
* class with 1,000 members from 100.
|
|
512
|
+
*/
|
|
513
|
+
function scanClass(pattern: string, index: number): ScannedAtom {
|
|
514
|
+
let end = index + 1;
|
|
515
|
+
let exotic = pattern[end] === '^';
|
|
516
|
+
if (exotic) end++;
|
|
517
|
+
let size = 0;
|
|
518
|
+
if (pattern[end] === ']') {
|
|
519
|
+
end++;
|
|
520
|
+
size++;
|
|
521
|
+
}
|
|
522
|
+
while (end < pattern.length && pattern[end] !== ']') {
|
|
523
|
+
if (pattern[end] === '\\') {
|
|
524
|
+
const escape = pattern.slice(end, escapeEnd(pattern, end));
|
|
525
|
+
exotic ||= isNonAsciiEscape(escape);
|
|
526
|
+
size += escapeSize(escape);
|
|
527
|
+
end += escape.length;
|
|
528
|
+
} else if (pattern[end] === '[') {
|
|
529
|
+
exotic ||= pattern.startsWith('[:^', end);
|
|
530
|
+
end = pattern.indexOf(':]', end) + 2;
|
|
531
|
+
size++;
|
|
532
|
+
} else {
|
|
533
|
+
exotic ||= pattern.charCodeAt(end) > 0x7f;
|
|
534
|
+
end++;
|
|
535
|
+
size++;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
return { end: end + 1, size: Math.max(exotic ? 100 : 2, size) };
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function scanAtom(pattern: string, index: number): ScannedAtom {
|
|
543
|
+
const char = pattern[index];
|
|
544
|
+
if (char === '[') return scanClass(pattern, index);
|
|
545
|
+
if (char === '\\') {
|
|
546
|
+
const end = escapeEnd(pattern, index);
|
|
547
|
+
return { end, size: escapeSize(pattern.slice(index, end)) };
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
return { end: index + 1, size: 1 };
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Whether Rust may reject a compiled pattern for exceeding its size limit,
|
|
555
|
+
* which RE2 does not have. Atoms add their size, a repetition multiplies its
|
|
556
|
+
* atom or group by its largest count, and alternatives add up. A `|` and a
|
|
557
|
+
* capturing group cost 1 each, as Rust fails from about 300,000 `|` and
|
|
558
|
+
* 110,000 `()`. Non-capturing and flag groups are free, as Rust accepts
|
|
559
|
+
* 2,000,000 `(?:)` and 1,000,000 `(?i)`. A repetition with a minimum of 0
|
|
560
|
+
* (`?`, `*`, `{0,n}`, `{0,}`, lazy or not) costs 1 more, like `(?:x|)`, as
|
|
561
|
+
* Rust fails from about 9,800 `.?` and 100,000 `(?:)*`. `{0}` is free.
|
|
562
|
+
*/
|
|
563
|
+
function exceedsRelayerRegexSize(pattern: string): boolean {
|
|
564
|
+
const groupSizes = [0];
|
|
565
|
+
let lastSize = 0;
|
|
566
|
+
let index = 0;
|
|
567
|
+
while (index < pattern.length) {
|
|
568
|
+
const char = pattern[index];
|
|
569
|
+
const rest = pattern.slice(index);
|
|
570
|
+
const flagGroup = FLAG_GROUP_REGEX.exec(rest);
|
|
571
|
+
const repetition = COUNTED_REPETITION_REGEX.exec(rest);
|
|
572
|
+
|
|
573
|
+
if (flagGroup) {
|
|
574
|
+
index += flagGroup[0].length;
|
|
575
|
+
lastSize = 0;
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
if (char === '(') {
|
|
579
|
+
const capturing =
|
|
580
|
+
!rest.startsWith('(?') || NAMED_GROUP_OPENER_REGEX.test(rest);
|
|
581
|
+
groupSizes.push(capturing ? 1 : 0);
|
|
582
|
+
index += /^\((\?[^:>]*[:>])?/.exec(rest)?.[0].length ?? 1;
|
|
583
|
+
lastSize = 0;
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
if (char === ')') {
|
|
587
|
+
lastSize = groupSizes.pop() ?? 0;
|
|
588
|
+
groupSizes[groupSizes.length - 1] += lastSize;
|
|
589
|
+
index++;
|
|
590
|
+
if (groupSizes.some((size) => size > MAX_RELAYER_REGEX_SIZE)) return true;
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
let copies: number | undefined;
|
|
595
|
+
let skippable = char === '*' || char === '?';
|
|
596
|
+
if (skippable) copies = 1;
|
|
597
|
+
else if (char === '+') copies = 2;
|
|
598
|
+
else if (char === '{' && repetition) {
|
|
599
|
+
const [, min, range, max] = repetition;
|
|
600
|
+
copies =
|
|
601
|
+
range === undefined ? Number(min) : max ? Number(max) : Number(min) + 1;
|
|
602
|
+
skippable = min === '0' && range !== undefined;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
if (copies !== undefined) {
|
|
606
|
+
index += char === '{' && repetition ? repetition[0].length : 1;
|
|
607
|
+
if (pattern[index] === '?') index++;
|
|
608
|
+
const surcharge = skippable ? 1 : 0;
|
|
609
|
+
groupSizes[groupSizes.length - 1] += lastSize * (copies - 1) + surcharge;
|
|
610
|
+
lastSize = lastSize * copies + surcharge;
|
|
611
|
+
} else {
|
|
612
|
+
const atom = scanAtom(pattern, index);
|
|
613
|
+
lastSize = atom.size;
|
|
614
|
+
groupSizes[groupSizes.length - 1] += lastSize;
|
|
615
|
+
index = atom.end;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
if (groupSizes.some((size) => size > MAX_RELAYER_REGEX_SIZE)) return true;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
return false;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Compiles `match.bodyRegex` with RE2, which like the relayer's Rust `regex`
|
|
626
|
+
* crate matches in linear time. Matching is an unanchored search over the
|
|
627
|
+
* lowercase hex body, so Unicode differences between the engines (RE2's
|
|
628
|
+
* ASCII `\d`, `\s` and `\b`, case folding) cannot change a match.
|
|
629
|
+
*
|
|
630
|
+
* Syntax RE2 accepts and Rust rejects or reads differently is rejected, so a
|
|
631
|
+
* file the relayer accepts with it is rejected too:
|
|
632
|
+
* - `\` followed by a digit, `\Q`, `\<`, `\>`, `\b{...}` and `\B{...}`;
|
|
633
|
+
* - a surrogate escape like `\x{d800}` and a negated property like
|
|
634
|
+
* `\p{^Greek}`;
|
|
635
|
+
* - a `{` that does not open a counted repetition without spaces or leading
|
|
636
|
+
* zeros, like `a{,3}`, `a{ 2}` or `a{02}`;
|
|
637
|
+
* - a group name not starting with a letter or `_`, and a quantifier right
|
|
638
|
+
* after a flag group like `(?i)*`;
|
|
639
|
+
* - in a class: nested classes other than POSIX ones like `[:alpha:]`, `&&`,
|
|
640
|
+
* `--`, `~~`, a range ending in a class (`-[`) and a range starting at an
|
|
641
|
+
* escaped class like `[\d-z]`;
|
|
642
|
+
* - groups nested deeper than {@link MAX_GROUP_DEPTH}, which keeps every
|
|
643
|
+
* accepted pattern within Rust's nesting limit.
|
|
644
|
+
*
|
|
645
|
+
* Rust-only syntax RE2 rejects is also rejected: `\u{...}`, the `x`, `R`
|
|
646
|
+
* and `-u` flags, stacked quantifiers like `a**` or `a{2}*`, counts above
|
|
647
|
+
* 1000, `\p{Script=...}`, unknown POSIX classes and group names with `.` or
|
|
648
|
+
* `[`.
|
|
649
|
+
*
|
|
650
|
+
* Patterns that may exceed Rust's compiled-size limit, like `\pL{260}`,
|
|
651
|
+
* `\w{220}` or `\D{900}`, are rejected too.
|
|
652
|
+
*/
|
|
653
|
+
function parseBodyRegex(value: unknown): RE2JS | undefined {
|
|
654
|
+
const pattern = parseOrThrow(BodyRegexSchema, value, 'match.bodyRegex');
|
|
655
|
+
if (isNullish(pattern)) return undefined;
|
|
656
|
+
|
|
657
|
+
if (divergesFromRelayerRegex(pattern)) {
|
|
658
|
+
throw new DenylistLoadError(
|
|
659
|
+
DenylistLoadFailure.InvalidFile,
|
|
660
|
+
"match.bodyRegex uses syntax the relayer's regex engine rejects or reads differently",
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
let regex: RE2JS;
|
|
665
|
+
try {
|
|
666
|
+
regex = RE2JS.compile(pattern);
|
|
667
|
+
} catch (error: unknown) {
|
|
668
|
+
throw new DenylistLoadError(
|
|
669
|
+
DenylistLoadFailure.InvalidFile,
|
|
670
|
+
`match.bodyRegex is not a valid regex: ${error instanceof Error ? error.message : String(error)}`,
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
if (exceedsRelayerRegexSize(pattern)) {
|
|
675
|
+
throw new DenylistLoadError(
|
|
676
|
+
DenylistLoadFailure.InvalidFile,
|
|
677
|
+
"match.bodyRegex may exceed the relayer's regex size limit",
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
return regex;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function parseMatch(
|
|
685
|
+
match: Record<string, unknown>,
|
|
686
|
+
reason: string,
|
|
687
|
+
expiresAt: number | undefined,
|
|
688
|
+
): DenylistMatchRule {
|
|
689
|
+
// The relayer accepts any key casing.
|
|
690
|
+
const normalized = new Map<string, unknown>();
|
|
691
|
+
for (const [key, value] of Object.entries(match)) {
|
|
692
|
+
const lowerKey = key.toLowerCase();
|
|
693
|
+
if (
|
|
694
|
+
normalized.has(lowerKey) &&
|
|
695
|
+
JSON.stringify(normalized.get(lowerKey)) !== JSON.stringify(value)
|
|
696
|
+
) {
|
|
697
|
+
throw new DenylistLoadError(
|
|
698
|
+
DenylistLoadFailure.InvalidFile,
|
|
699
|
+
`Conflicting \`match\` aliases for \`${lowerKey}\``,
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
normalized.set(lowerKey, value);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
const knownKeys: ReadonlySet<string> = new Set(Object.values(MATCH_KEYS));
|
|
706
|
+
for (const key of normalized.keys()) {
|
|
707
|
+
if (!knownKeys.has(key)) {
|
|
708
|
+
throw new DenylistLoadError(
|
|
709
|
+
DenylistLoadFailure.InvalidFile,
|
|
710
|
+
`Unknown \`match\` field \`${key}\``,
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
const field = (key: string) =>
|
|
716
|
+
normalized.has(key) ? normalized.get(key) : WILDCARD;
|
|
717
|
+
|
|
718
|
+
const rule: DenylistMatchRule = {
|
|
719
|
+
reason,
|
|
720
|
+
expiresAt,
|
|
721
|
+
messageIds: parseAddressFilter(
|
|
722
|
+
field(MATCH_KEYS.messageId),
|
|
723
|
+
'match.messageId',
|
|
724
|
+
),
|
|
725
|
+
originDomains: parseDomainFilter(
|
|
726
|
+
field(MATCH_KEYS.originDomain),
|
|
727
|
+
'match.originDomain',
|
|
728
|
+
),
|
|
729
|
+
senders: parseAddressFilter(
|
|
730
|
+
field(MATCH_KEYS.senderAddress),
|
|
731
|
+
'match.senderAddress',
|
|
732
|
+
),
|
|
733
|
+
destinationDomains: parseDomainFilter(
|
|
734
|
+
field(MATCH_KEYS.destinationDomain),
|
|
735
|
+
'match.destinationDomain',
|
|
736
|
+
),
|
|
737
|
+
recipients: parseAddressFilter(
|
|
738
|
+
field(MATCH_KEYS.recipientAddress),
|
|
739
|
+
'match.recipientAddress',
|
|
740
|
+
),
|
|
741
|
+
bodyRegex: parseBodyRegex(normalized.get(MATCH_KEYS.bodyRegex)),
|
|
742
|
+
};
|
|
743
|
+
|
|
744
|
+
if (
|
|
745
|
+
!rule.messageIds &&
|
|
746
|
+
!rule.originDomains &&
|
|
747
|
+
!rule.senders &&
|
|
748
|
+
!rule.destinationDomains &&
|
|
749
|
+
!rule.recipients
|
|
750
|
+
) {
|
|
751
|
+
throw new DenylistLoadError(
|
|
752
|
+
DenylistLoadFailure.InvalidFile,
|
|
753
|
+
'`match` must set messageId, originDomain, senderAddress, destinationDomain or recipientAddress; otherwise it can block every message',
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
return rule;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function parseAddressRule(
|
|
761
|
+
value: string,
|
|
762
|
+
reason: string,
|
|
763
|
+
expiresAt: number | undefined,
|
|
764
|
+
matchBody: boolean | undefined,
|
|
765
|
+
): DenylistAddressRule {
|
|
766
|
+
const bodyAddress = parseMatchBodyAddress(value, matchBody);
|
|
767
|
+
const address = parseDenylistAddress(value.trim());
|
|
768
|
+
|
|
769
|
+
if (address === ZERO_ADDRESS_HEX_32) {
|
|
770
|
+
throw new DenylistLoadError(
|
|
771
|
+
DenylistLoadFailure.InvalidFile,
|
|
772
|
+
'`address` must not be zero',
|
|
773
|
+
);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
return { reason, expiresAt, address, bodyAddress };
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function parseRule(raw: unknown, rules: DenylistRules): void {
|
|
780
|
+
const rule = parseOrThrow(DenylistRuleSchema, raw, 'rule');
|
|
781
|
+
const reason = `${rule.reason.trim()} (added by ${rule.addedBy.trim()})`;
|
|
782
|
+
const expiresAt = isNullish(rule.expiresAt)
|
|
783
|
+
? undefined
|
|
784
|
+
: parseRfc3339(rule.expiresAt);
|
|
785
|
+
|
|
786
|
+
if (!isNullish(rule.match)) {
|
|
787
|
+
rules.matchRules.push(parseMatch(rule.match, reason, expiresAt));
|
|
788
|
+
} else if (!isNullish(rule.address)) {
|
|
789
|
+
rules.addressRules.push(
|
|
790
|
+
parseAddressRule(rule.address, reason, expiresAt, rule.matchBody),
|
|
791
|
+
);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/** The third `JSON.parse` reviver argument; `source` is set on primitives. */
|
|
796
|
+
interface JsonReviverContext {
|
|
797
|
+
source?: string;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* serde_json reads `1.0`, `1e3` and `-0` as floats, which the relayer rejects
|
|
802
|
+
* wherever the file allows a number, but `JSON.parse` returns integers for them.
|
|
803
|
+
*/
|
|
804
|
+
function rejectNonIntegerLiterals(
|
|
805
|
+
_key: string,
|
|
806
|
+
value: unknown,
|
|
807
|
+
context?: JsonReviverContext,
|
|
808
|
+
): unknown {
|
|
809
|
+
if (typeof value !== 'number') return value;
|
|
810
|
+
|
|
811
|
+
const source = context?.source;
|
|
812
|
+
assert(source !== undefined, 'JSON.parse does not expose number source text');
|
|
813
|
+
if (!/^(0|[1-9]\d*)$/.test(source)) {
|
|
814
|
+
throw new Error(`number \`${source}\` is not a non-negative integer`);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
return value;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
/**
|
|
821
|
+
* Parses a runtime denylist file. Throws a `DenylistLoadError` if any rule is
|
|
822
|
+
* invalid. Expired rules are kept and skipped at match time.
|
|
823
|
+
*/
|
|
824
|
+
export function parseDenylistFile(text: string): DenylistRules {
|
|
825
|
+
let json: unknown;
|
|
826
|
+
try {
|
|
827
|
+
json = JSON.parse(text, rejectNonIntegerLiterals);
|
|
828
|
+
} catch (error: unknown) {
|
|
829
|
+
throw new DenylistLoadError(
|
|
830
|
+
DenylistLoadFailure.InvalidFile,
|
|
831
|
+
`Invalid denylist JSON: ${error instanceof Error ? error.message : String(error)}`,
|
|
832
|
+
);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
const file = parseOrThrow(DenylistFileSchema, json, 'file');
|
|
836
|
+
const rules: DenylistRules = { matchRules: [], addressRules: [] };
|
|
837
|
+
|
|
838
|
+
file.rules.forEach((raw, index) => {
|
|
839
|
+
try {
|
|
840
|
+
parseRule(raw, rules);
|
|
841
|
+
} catch (error: unknown) {
|
|
842
|
+
const failure =
|
|
843
|
+
error instanceof DenylistLoadError
|
|
844
|
+
? error.failure
|
|
845
|
+
: DenylistLoadFailure.InvalidFile;
|
|
846
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
847
|
+
throw new DenylistLoadError(
|
|
848
|
+
failure,
|
|
849
|
+
`Invalid denylist rule at index ${index}: ${message}`,
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
});
|
|
853
|
+
|
|
854
|
+
return rules;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
function isExpired(expiresAt: number | undefined, nowMs: number): boolean {
|
|
858
|
+
return expiresAt !== undefined && expiresAt <= nowMs;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
function matchesFilter<T>(
|
|
862
|
+
filter: ReadonlySet<T> | undefined,
|
|
863
|
+
value: T,
|
|
864
|
+
): boolean {
|
|
865
|
+
return filter === undefined || filter.has(value);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
/**
|
|
869
|
+
* Returns the reason of the first active rule that denies the message, like
|
|
870
|
+
* the relayer: `match` rules first, then `address` rules against the sender,
|
|
871
|
+
* the recipient, the warp transfer recipient (first 32 body bytes), the
|
|
872
|
+
* origin tx sender and, for `matchBody` rules, anywhere in the body.
|
|
873
|
+
*/
|
|
874
|
+
export function findDenyReason(
|
|
875
|
+
rules: DenylistRules,
|
|
876
|
+
msg: ExplorerMessage,
|
|
877
|
+
nowMs: number,
|
|
878
|
+
): string | undefined {
|
|
879
|
+
const messageId = normalizeMessageId(msg.msg_id);
|
|
880
|
+
const sender = addressToBytes32(msg.sender).toLowerCase();
|
|
881
|
+
const recipient = addressToBytes32(msg.recipient).toLowerCase();
|
|
882
|
+
const body = strip0x(msg.message_body).toLowerCase();
|
|
883
|
+
|
|
884
|
+
const matchRule = rules.matchRules.find(
|
|
885
|
+
(rule) =>
|
|
886
|
+
!isExpired(rule.expiresAt, nowMs) &&
|
|
887
|
+
matchesFilter(rule.messageIds, messageId) &&
|
|
888
|
+
matchesFilter(rule.originDomains, msg.origin_domain_id) &&
|
|
889
|
+
matchesFilter(rule.senders, sender) &&
|
|
890
|
+
matchesFilter(rule.destinationDomains, msg.destination_domain_id) &&
|
|
891
|
+
matchesFilter(rule.recipients, recipient) &&
|
|
892
|
+
(rule.bodyRegex === undefined || rule.bodyRegex.test(body)),
|
|
893
|
+
);
|
|
894
|
+
if (matchRule) return matchRule.reason;
|
|
895
|
+
|
|
896
|
+
const candidates = new Set([sender, recipient]);
|
|
897
|
+
if (body.length >= 64) {
|
|
898
|
+
candidates.add(addressToBytes32(body.slice(0, 64)).toLowerCase());
|
|
899
|
+
}
|
|
900
|
+
if (msg.origin_tx_sender) {
|
|
901
|
+
candidates.add(addressToBytes32(msg.origin_tx_sender).toLowerCase());
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
return rules.addressRules.find(
|
|
905
|
+
(rule) =>
|
|
906
|
+
!isExpired(rule.expiresAt, nowMs) &&
|
|
907
|
+
(candidates.has(rule.address) ||
|
|
908
|
+
(rule.bodyAddress !== undefined &&
|
|
909
|
+
bodyContainsAddress(msg.message_body, rule.bodyAddress))),
|
|
910
|
+
)?.reason;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
/**
|
|
914
|
+
* Returns the reason of the first active `match` rule that sets only
|
|
915
|
+
* `messageId` and lists the message. Other rules are ignored.
|
|
916
|
+
*/
|
|
917
|
+
export function findMessageIdDenyReason(
|
|
918
|
+
rules: DenylistRules,
|
|
919
|
+
messageId: string,
|
|
920
|
+
nowMs: number,
|
|
921
|
+
): string | undefined {
|
|
922
|
+
const id = normalizeMessageId(messageId);
|
|
923
|
+
|
|
924
|
+
return rules.matchRules.find(
|
|
925
|
+
(rule) =>
|
|
926
|
+
!isExpired(rule.expiresAt, nowMs) &&
|
|
927
|
+
rule.messageIds?.has(id) &&
|
|
928
|
+
rule.originDomains === undefined &&
|
|
929
|
+
rule.senders === undefined &&
|
|
930
|
+
rule.destinationDomains === undefined &&
|
|
931
|
+
rule.recipients === undefined &&
|
|
932
|
+
rule.bodyRegex === undefined,
|
|
933
|
+
)?.reason;
|
|
934
|
+
}
|