@humanode-hyperlane/rebalancer 27.2.2 → 1029.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bridges/LiFiBridge.d.ts +3 -3
- package/dist/bridges/LiFiBridge.d.ts.map +1 -1
- package/dist/bridges/LiFiBridge.js +66 -55
- package/dist/bridges/LiFiBridge.js.map +1 -1
- package/dist/bridges/LiFiBridge.test.js +213 -0
- package/dist/bridges/LiFiBridge.test.js.map +1 -1
- package/dist/config/RebalancerConfig.d.ts +4 -3
- package/dist/config/RebalancerConfig.d.ts.map +1 -1
- package/dist/config/RebalancerConfig.js +7 -5
- package/dist/config/RebalancerConfig.js.map +1 -1
- package/dist/config/RebalancerConfig.test.js +278 -2
- package/dist/config/RebalancerConfig.test.js.map +1 -1
- package/dist/config/types.d.ts +707 -2664
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/types.js +62 -7
- package/dist/config/types.js.map +1 -1
- package/dist/core/InventoryRebalancer.d.ts +13 -19
- package/dist/core/InventoryRebalancer.d.ts.map +1 -1
- package/dist/core/InventoryRebalancer.js +404 -276
- package/dist/core/InventoryRebalancer.js.map +1 -1
- package/dist/core/InventoryRebalancer.test.js +814 -25
- package/dist/core/InventoryRebalancer.test.js.map +1 -1
- package/dist/core/Rebalancer.d.ts.map +1 -1
- package/dist/core/Rebalancer.js +12 -6
- package/dist/core/Rebalancer.js.map +1 -1
- package/dist/core/Rebalancer.test.js +51 -0
- package/dist/core/Rebalancer.test.js.map +1 -1
- package/dist/core/RebalancerOrchestrator.d.ts.map +1 -1
- package/dist/core/RebalancerOrchestrator.test.js +0 -1
- package/dist/core/RebalancerOrchestrator.test.js.map +1 -1
- package/dist/core/RebalancerService.d.ts.map +1 -1
- package/dist/core/RebalancerService.js +3 -2
- package/dist/core/RebalancerService.js.map +1 -1
- package/dist/core/RebalancerService.test.js +24 -0
- package/dist/core/RebalancerService.test.js.map +1 -1
- package/dist/e2e/fixtures/routes.d.ts +3 -3
- package/dist/e2e/fixtures/routes.d.ts.map +1 -1
- package/dist/e2e/harness/BaseLocalDeploymentManager.d.ts.map +1 -1
- package/dist/e2e/harness/BaseLocalDeploymentManager.js +3 -2
- package/dist/e2e/harness/BaseLocalDeploymentManager.js.map +1 -1
- package/dist/e2e/harness/Erc20InventoryLocalDeploymentManager.d.ts.map +1 -1
- package/dist/e2e/harness/Erc20InventoryLocalDeploymentManager.js.map +1 -1
- package/dist/e2e/harness/Erc20LocalDeploymentManager.d.ts.map +1 -1
- package/dist/e2e/harness/ForkIndexer.d.ts.map +1 -1
- package/dist/e2e/harness/ForkIndexer.js.map +1 -1
- package/dist/e2e/harness/LocalProvider.d.ts +3 -0
- package/dist/e2e/harness/LocalProvider.d.ts.map +1 -0
- package/dist/e2e/harness/LocalProvider.js +9 -0
- package/dist/e2e/harness/LocalProvider.js.map +1 -0
- package/dist/e2e/harness/LocalProvider.test.d.ts +2 -0
- package/dist/e2e/harness/LocalProvider.test.d.ts.map +1 -0
- package/dist/e2e/harness/LocalProvider.test.js +14 -0
- package/dist/e2e/harness/LocalProvider.test.js.map +1 -0
- package/dist/e2e/harness/MixedLocalDeploymentManager.d.ts.map +1 -1
- package/dist/e2e/harness/MixedLocalDeploymentManager.js.map +1 -1
- package/dist/e2e/harness/MockExplorerClient.d.ts.map +1 -1
- package/dist/e2e/harness/MockExplorerClient.js.map +1 -1
- package/dist/e2e/harness/MockExternalBridge.d.ts.map +1 -1
- package/dist/e2e/harness/MockExternalBridge.js.map +1 -1
- package/dist/e2e/harness/NativeLocalDeploymentManager.d.ts.map +1 -1
- package/dist/e2e/harness/NativeLocalDeploymentManager.js +2 -1
- package/dist/e2e/harness/NativeLocalDeploymentManager.js.map +1 -1
- package/dist/e2e/harness/SnapshotHelper.d.ts.map +1 -1
- package/dist/e2e/harness/SnapshotHelper.js +3 -5
- package/dist/e2e/harness/SnapshotHelper.js.map +1 -1
- package/dist/e2e/harness/TestHelpers.js +1 -2
- package/dist/e2e/harness/TestHelpers.js.map +1 -1
- package/dist/e2e/harness/TestRebalancer.d.ts +5 -1
- package/dist/e2e/harness/TestRebalancer.d.ts.map +1 -1
- package/dist/e2e/harness/TestRebalancer.js +14 -4
- package/dist/e2e/harness/TestRebalancer.js.map +1 -1
- package/dist/e2e/relayer-denylist.e2e-test.d.ts +2 -0
- package/dist/e2e/relayer-denylist.e2e-test.d.ts.map +1 -0
- package/dist/e2e/relayer-denylist.e2e-test.js +388 -0
- package/dist/e2e/relayer-denylist.e2e-test.js.map +1 -0
- package/dist/factories/RebalancerContextFactory.d.ts.map +1 -1
- package/dist/factories/RebalancerContextFactory.js +61 -31
- package/dist/factories/RebalancerContextFactory.js.map +1 -1
- package/dist/factories/RebalancerContextFactory.test.js +336 -5
- package/dist/factories/RebalancerContextFactory.test.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/interfaces/IRebalancer.d.ts +4 -2
- package/dist/interfaces/IRebalancer.d.ts.map +1 -1
- package/dist/metrics/Metrics.d.ts.map +1 -1
- package/dist/metrics/Metrics.js +1 -1
- package/dist/metrics/Metrics.js.map +1 -1
- package/dist/metrics/PriceGetter.d.ts +1 -0
- package/dist/metrics/PriceGetter.d.ts.map +1 -1
- package/dist/metrics/PriceGetter.js +14 -4
- package/dist/metrics/PriceGetter.js.map +1 -1
- package/dist/metrics/PriceGetter.test.d.ts +2 -0
- package/dist/metrics/PriceGetter.test.d.ts.map +1 -0
- package/dist/metrics/PriceGetter.test.js +84 -0
- package/dist/metrics/PriceGetter.test.js.map +1 -0
- package/dist/metrics/scripts/metrics.d.ts +4 -4
- package/dist/monitor/Monitor.d.ts +1 -0
- package/dist/monitor/Monitor.d.ts.map +1 -1
- package/dist/monitor/Monitor.js +55 -46
- package/dist/monitor/Monitor.js.map +1 -1
- package/dist/monitor/Monitor.test.d.ts +2 -0
- package/dist/monitor/Monitor.test.d.ts.map +1 -0
- package/dist/monitor/Monitor.test.js +346 -0
- package/dist/monitor/Monitor.test.js.map +1 -0
- package/dist/service.d.ts +1 -1
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +9 -5
- package/dist/service.js.map +1 -1
- package/dist/strategy/BaseStrategy.d.ts.map +1 -1
- package/dist/strategy/BaseStrategy.js +13 -11
- package/dist/strategy/BaseStrategy.js.map +1 -1
- package/dist/strategy/CollateralDeficitStrategy.d.ts.map +1 -1
- package/dist/strategy/CollateralDeficitStrategy.js +2 -2
- package/dist/strategy/CollateralDeficitStrategy.js.map +1 -1
- package/dist/strategy/CompositeStrategy.d.ts.map +1 -1
- package/dist/strategy/CompositeStrategy.js.map +1 -1
- package/dist/strategy/CompositeStrategy.test.js.map +1 -1
- package/dist/strategy/MinAmountStrategy.d.ts +1 -0
- package/dist/strategy/MinAmountStrategy.d.ts.map +1 -1
- package/dist/strategy/MinAmountStrategy.js +12 -8
- package/dist/strategy/MinAmountStrategy.js.map +1 -1
- package/dist/strategy/MinAmountStrategy.test.js +189 -2
- package/dist/strategy/MinAmountStrategy.test.js.map +1 -1
- package/dist/strategy/StrategyFactory.d.ts.map +1 -1
- package/dist/strategy/StrategyFactory.js.map +1 -1
- package/dist/strategy/WeightedStrategy.d.ts.map +1 -1
- package/dist/test/helpers.d.ts +11 -3
- package/dist/test/helpers.d.ts.map +1 -1
- package/dist/test/helpers.js +9 -11
- package/dist/test/helpers.js.map +1 -1
- package/dist/test/lifiMocks.d.ts +6 -6
- package/dist/test/lifiMocks.d.ts.map +1 -1
- package/dist/test/lifiMocks.js +5 -2
- package/dist/test/lifiMocks.js.map +1 -1
- package/dist/test/pendingTransferMiddlewareMocks.d.ts +17 -0
- package/dist/test/pendingTransferMiddlewareMocks.d.ts.map +1 -0
- package/dist/test/pendingTransferMiddlewareMocks.js +43 -0
- package/dist/test/pendingTransferMiddlewareMocks.js.map +1 -0
- package/dist/tracking/ActionTracker.d.ts +4 -0
- package/dist/tracking/ActionTracker.d.ts.map +1 -1
- package/dist/tracking/ActionTracker.js +76 -9
- package/dist/tracking/ActionTracker.js.map +1 -1
- package/dist/tracking/ActionTracker.test.js +418 -1
- package/dist/tracking/ActionTracker.test.js.map +1 -1
- package/dist/tracking/IActionTracker.d.ts +4 -2
- package/dist/tracking/IActionTracker.d.ts.map +1 -1
- package/dist/tracking/InflightContextAdapter.d.ts.map +1 -1
- package/dist/tracking/InflightContextAdapter.js.map +1 -1
- package/dist/tracking/pendingTransferMiddlewares/BlacklistIsmMiddleware.d.ts +64 -0
- package/dist/tracking/pendingTransferMiddlewares/BlacklistIsmMiddleware.d.ts.map +1 -0
- package/dist/tracking/pendingTransferMiddlewares/BlacklistIsmMiddleware.js +474 -0
- package/dist/tracking/pendingTransferMiddlewares/BlacklistIsmMiddleware.js.map +1 -0
- package/dist/tracking/pendingTransferMiddlewares/BlacklistIsmMiddleware.test.d.ts +2 -0
- package/dist/tracking/pendingTransferMiddlewares/BlacklistIsmMiddleware.test.d.ts.map +1 -0
- package/dist/tracking/pendingTransferMiddlewares/BlacklistIsmMiddleware.test.js +1168 -0
- package/dist/tracking/pendingTransferMiddlewares/BlacklistIsmMiddleware.test.js.map +1 -0
- package/dist/tracking/pendingTransferMiddlewares/PendingTransferMiddleware.d.ts +61 -0
- package/dist/tracking/pendingTransferMiddlewares/PendingTransferMiddleware.d.ts.map +1 -0
- package/dist/tracking/pendingTransferMiddlewares/PendingTransferMiddleware.js +160 -0
- package/dist/tracking/pendingTransferMiddlewares/PendingTransferMiddleware.js.map +1 -0
- package/dist/tracking/pendingTransferMiddlewares/PendingTransferMiddleware.test.d.ts +2 -0
- package/dist/tracking/pendingTransferMiddlewares/PendingTransferMiddleware.test.d.ts.map +1 -0
- package/dist/tracking/pendingTransferMiddlewares/PendingTransferMiddleware.test.js +369 -0
- package/dist/tracking/pendingTransferMiddlewares/PendingTransferMiddleware.test.js.map +1 -0
- package/dist/tracking/pendingTransferMiddlewares/RelayerDenylistMiddleware.d.ts +60 -0
- package/dist/tracking/pendingTransferMiddlewares/RelayerDenylistMiddleware.d.ts.map +1 -0
- package/dist/tracking/pendingTransferMiddlewares/RelayerDenylistMiddleware.js +288 -0
- package/dist/tracking/pendingTransferMiddlewares/RelayerDenylistMiddleware.js.map +1 -0
- package/dist/tracking/pendingTransferMiddlewares/RelayerDenylistMiddleware.test.d.ts +2 -0
- package/dist/tracking/pendingTransferMiddlewares/RelayerDenylistMiddleware.test.d.ts.map +1 -0
- package/dist/tracking/pendingTransferMiddlewares/RelayerDenylistMiddleware.test.js +606 -0
- package/dist/tracking/pendingTransferMiddlewares/RelayerDenylistMiddleware.test.js.map +1 -0
- package/dist/tracking/pendingTransferMiddlewares/buildPendingTransferMiddlewares.d.ts +25 -0
- package/dist/tracking/pendingTransferMiddlewares/buildPendingTransferMiddlewares.d.ts.map +1 -0
- package/dist/tracking/pendingTransferMiddlewares/buildPendingTransferMiddlewares.js +41 -0
- package/dist/tracking/pendingTransferMiddlewares/buildPendingTransferMiddlewares.js.map +1 -0
- package/dist/tracking/pendingTransferMiddlewares/buildPendingTransferMiddlewares.test.d.ts +2 -0
- package/dist/tracking/pendingTransferMiddlewares/buildPendingTransferMiddlewares.test.d.ts.map +1 -0
- package/dist/tracking/pendingTransferMiddlewares/buildPendingTransferMiddlewares.test.js +141 -0
- package/dist/tracking/pendingTransferMiddlewares/buildPendingTransferMiddlewares.test.js.map +1 -0
- package/dist/tracking/pendingTransferMiddlewares/matchBody.d.ts +12 -0
- package/dist/tracking/pendingTransferMiddlewares/matchBody.d.ts.map +1 -0
- package/dist/tracking/pendingTransferMiddlewares/matchBody.js +29 -0
- package/dist/tracking/pendingTransferMiddlewares/matchBody.js.map +1 -0
- package/dist/tracking/pendingTransferMiddlewares/relayerDenylistRules.d.ts +59 -0
- package/dist/tracking/pendingTransferMiddlewares/relayerDenylistRules.d.ts.map +1 -0
- package/dist/tracking/pendingTransferMiddlewares/relayerDenylistRules.js +661 -0
- package/dist/tracking/pendingTransferMiddlewares/relayerDenylistRules.js.map +1 -0
- package/dist/tracking/pendingTransferMiddlewares/relayerDenylistRules.test.d.ts +2 -0
- package/dist/tracking/pendingTransferMiddlewares/relayerDenylistRules.test.d.ts.map +1 -0
- package/dist/tracking/pendingTransferMiddlewares/relayerDenylistRules.test.js +1093 -0
- package/dist/tracking/pendingTransferMiddlewares/relayerDenylistRules.test.js.map +1 -0
- package/dist/tracking/pendingTransferMiddlewares/types.d.ts +2 -0
- package/dist/tracking/pendingTransferMiddlewares/types.d.ts.map +1 -0
- package/dist/tracking/pendingTransferMiddlewares/types.js +4 -0
- package/dist/tracking/pendingTransferMiddlewares/types.js.map +1 -0
- package/dist/tracking/store/InMemoryStore.d.ts.map +1 -1
- package/dist/utils/ExplorerClient.d.ts +1 -0
- package/dist/utils/ExplorerClient.d.ts.map +1 -1
- package/dist/utils/ExplorerClient.js +8 -3
- package/dist/utils/ExplorerClient.js.map +1 -1
- package/dist/utils/ExplorerClient.test.js +25 -0
- package/dist/utils/ExplorerClient.test.js.map +1 -1
- package/dist/utils/balanceUtils.d.ts +7 -1
- package/dist/utils/balanceUtils.d.ts.map +1 -1
- package/dist/utils/balanceUtils.js +39 -1
- package/dist/utils/balanceUtils.js.map +1 -1
- package/dist/utils/balanceUtils.test.js +55 -1
- package/dist/utils/balanceUtils.test.js.map +1 -1
- package/dist/utils/blockTag.d.ts +5 -4
- package/dist/utils/blockTag.d.ts.map +1 -1
- package/dist/utils/blockTag.js +26 -10
- package/dist/utils/blockTag.js.map +1 -1
- package/dist/utils/blockTag.test.d.ts +2 -0
- package/dist/utils/blockTag.test.d.ts.map +1 -0
- package/dist/utils/blockTag.test.js +132 -0
- package/dist/utils/blockTag.test.js.map +1 -0
- package/dist/utils/bridgeUtils.js.map +1 -1
- package/dist/utils/errors.d.ts.map +1 -1
- package/dist/utils/gasEstimation.d.ts +2 -1
- package/dist/utils/gasEstimation.d.ts.map +1 -1
- package/dist/utils/gasEstimation.js +16 -13
- package/dist/utils/gasEstimation.js.map +1 -1
- package/dist/utils/gasEstimation.test.d.ts +2 -0
- package/dist/utils/gasEstimation.test.d.ts.map +1 -0
- package/dist/utils/gasEstimation.test.js +104 -0
- package/dist/utils/gasEstimation.test.js.map +1 -0
- package/dist/utils/tokenUtils.d.ts.map +1 -1
- package/dist/utils/tokenUtils.js +5 -2
- package/dist/utils/tokenUtils.js.map +1 -1
- package/package.json +23 -22
- package/src/bridges/LiFiBridge.test.ts +270 -0
- package/src/bridges/LiFiBridge.ts +90 -71
- package/src/config/RebalancerConfig.test.ts +315 -1
- package/src/config/RebalancerConfig.ts +6 -3
- package/src/config/types.ts +98 -8
- package/src/core/InventoryRebalancer.test.ts +1092 -38
- package/src/core/InventoryRebalancer.ts +584 -351
- package/src/core/Rebalancer.test.ts +84 -0
- package/src/core/Rebalancer.ts +22 -6
- package/src/core/RebalancerOrchestrator.test.ts +0 -1
- package/src/core/RebalancerService.test.ts +35 -0
- package/src/core/RebalancerService.ts +3 -2
- package/src/e2e/harness/BaseLocalDeploymentManager.ts +3 -1
- package/src/e2e/harness/LocalProvider.test.ts +16 -0
- package/src/e2e/harness/LocalProvider.ts +10 -0
- package/src/e2e/harness/NativeLocalDeploymentManager.ts +2 -1
- package/src/e2e/harness/SnapshotHelper.ts +2 -5
- package/src/e2e/harness/TestHelpers.ts +3 -3
- package/src/e2e/harness/TestRebalancer.ts +21 -1
- package/src/e2e/relayer-denylist.e2e-test.ts +558 -0
- package/src/factories/RebalancerContextFactory.test.ts +489 -8
- package/src/factories/RebalancerContextFactory.ts +86 -36
- package/src/index.ts +2 -0
- package/src/interfaces/IRebalancer.ts +4 -1
- package/src/metrics/Metrics.ts +1 -0
- package/src/metrics/PriceGetter.test.ts +113 -0
- package/src/metrics/PriceGetter.ts +18 -3
- package/src/monitor/Monitor.test.ts +408 -0
- package/src/monitor/Monitor.ts +123 -97
- package/src/service.ts +14 -8
- package/src/strategy/BaseStrategy.ts +18 -15
- package/src/strategy/CollateralDeficitStrategy.ts +4 -3
- package/src/strategy/MinAmountStrategy.test.ts +238 -2
- package/src/strategy/MinAmountStrategy.ts +29 -17
- package/src/test/helpers.ts +13 -12
- package/src/test/lifiMocks.ts +5 -2
- package/src/test/pendingTransferMiddlewareMocks.ts +65 -0
- package/src/tracking/ActionTracker.test.ts +635 -3
- package/src/tracking/ActionTracker.ts +92 -12
- package/src/tracking/IActionTracker.ts +4 -2
- package/src/tracking/pendingTransferMiddlewares/BlacklistIsmMiddleware.test.ts +1693 -0
- package/src/tracking/pendingTransferMiddlewares/BlacklistIsmMiddleware.ts +784 -0
- package/src/tracking/pendingTransferMiddlewares/PendingTransferMiddleware.test.ts +554 -0
- package/src/tracking/pendingTransferMiddlewares/PendingTransferMiddleware.ts +233 -0
- package/src/tracking/pendingTransferMiddlewares/RelayerDenylistMiddleware.test.ts +894 -0
- package/src/tracking/pendingTransferMiddlewares/RelayerDenylistMiddleware.ts +404 -0
- package/src/tracking/pendingTransferMiddlewares/buildPendingTransferMiddlewares.test.ts +205 -0
- package/src/tracking/pendingTransferMiddlewares/buildPendingTransferMiddlewares.ts +89 -0
- package/src/tracking/pendingTransferMiddlewares/fixtures/mainnet-denylist.json +3556 -0
- package/src/tracking/pendingTransferMiddlewares/matchBody.ts +43 -0
- package/src/tracking/pendingTransferMiddlewares/relayerDenylistRules.test.ts +1220 -0
- package/src/tracking/pendingTransferMiddlewares/relayerDenylistRules.ts +934 -0
- package/src/tracking/pendingTransferMiddlewares/types.ts +3 -0
- package/src/utils/ExplorerClient.test.ts +42 -0
- package/src/utils/ExplorerClient.ts +14 -3
- package/src/utils/balanceUtils.test.ts +87 -1
- package/src/utils/balanceUtils.ts +73 -2
- package/src/utils/blockTag.test.ts +173 -0
- package/src/utils/blockTag.ts +35 -15
- package/src/utils/gasEstimation.test.ts +192 -0
- package/src/utils/gasEstimation.ts +17 -11
- package/src/utils/tokenUtils.ts +5 -2
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
import chai, { expect } from 'chai';
|
|
2
2
|
import chaiAsPromised from 'chai-as-promised';
|
|
3
3
|
import { Wallet } from 'ethers';
|
|
4
|
+
import type { Logger } from 'pino';
|
|
4
5
|
import { pino } from 'pino';
|
|
5
6
|
import Sinon, { type SinonStubbedInstance } from 'sinon';
|
|
6
7
|
|
|
7
8
|
import {
|
|
8
9
|
type ChainName,
|
|
10
|
+
HyperlaneCore,
|
|
9
11
|
type MultiProvider,
|
|
10
12
|
ProviderType,
|
|
11
13
|
TokenStandard,
|
|
12
14
|
WarpTxCategory,
|
|
13
15
|
type WarpCore,
|
|
14
16
|
} from '@humanode-hyperlane/sdk';
|
|
15
|
-
import { ProtocolType } from '@humanode-hyperlane/utils';
|
|
17
|
+
import { assert, ProtocolType } from '@humanode-hyperlane/utils';
|
|
16
18
|
|
|
17
19
|
import { ExternalBridgeType } from '../config/types.js';
|
|
18
20
|
import type { IExternalBridge } from '../interfaces/IExternalBridge.js';
|
|
@@ -154,6 +156,9 @@ describe('InventoryRebalancer E2E', () => {
|
|
|
154
156
|
}),
|
|
155
157
|
},
|
|
156
158
|
findToken: Sinon.stub().returns(null),
|
|
159
|
+
getMaxTransferAmount: Sinon.stub().callsFake(
|
|
160
|
+
async ({ balance }) => balance,
|
|
161
|
+
),
|
|
157
162
|
getTransferRemoteTxs: Sinon.stub().resolves([
|
|
158
163
|
{
|
|
159
164
|
category: 'transfer',
|
|
@@ -277,6 +282,21 @@ describe('InventoryRebalancer E2E', () => {
|
|
|
277
282
|
return intent;
|
|
278
283
|
}
|
|
279
284
|
|
|
285
|
+
function mockSuccessfulBridge(fromAmount: bigint, toAmount: bigint): void {
|
|
286
|
+
bridge.quote.resolves(
|
|
287
|
+
createMockBridgeQuote({
|
|
288
|
+
fromAmount,
|
|
289
|
+
toAmount,
|
|
290
|
+
toAmountMin: toAmount,
|
|
291
|
+
}),
|
|
292
|
+
);
|
|
293
|
+
bridge.execute.resolves({
|
|
294
|
+
txHash: '0xBridgeTxHash',
|
|
295
|
+
fromChain: 42161,
|
|
296
|
+
toChain: 1399811149,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
280
300
|
describe('Basic Inventory Rebalance (Sufficient Inventory)', () => {
|
|
281
301
|
// NOTE: Strategy route is arbitrum (surplus) → solana (deficit)
|
|
282
302
|
// But execution calls transferRemote FROM solana TO arbitrum (swapped direction)
|
|
@@ -396,11 +416,198 @@ describe('InventoryRebalancer E2E', () => {
|
|
|
396
416
|
const actionParams = actionTracker.createRebalanceAction.lastCall.args[0];
|
|
397
417
|
expect(actionParams.txHash).to.equal('0xSolanaTxHash');
|
|
398
418
|
});
|
|
419
|
+
|
|
420
|
+
it('denormalizes inventory execution amounts but records canonical deposit amount', async () => {
|
|
421
|
+
const route = createTestRoute({ amount: 1_000_000n });
|
|
422
|
+
createTestIntent({ amount: 1_000_000n });
|
|
423
|
+
warpCore.tokens.find((t: any) => t.chainName === SOLANA_CHAIN)!.scale = {
|
|
424
|
+
numerator: 1n,
|
|
425
|
+
denominator: 1_000_000_000_000n,
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
inventoryRebalancer.setInventoryBalances({
|
|
429
|
+
[SOLANA_CHAIN]: 1_000_000_000_000_000_000n,
|
|
430
|
+
[ARBITRUM_CHAIN]: 0n,
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
const results = await inventoryRebalancer.rebalance([route]);
|
|
434
|
+
|
|
435
|
+
expect(results).to.have.lengthOf(1);
|
|
436
|
+
expect(results[0].success).to.be.true;
|
|
437
|
+
|
|
438
|
+
const txParams = warpCore.getTransferRemoteTxs.firstCall.args[0];
|
|
439
|
+
expect(txParams.originTokenAmount.amount).to.equal(
|
|
440
|
+
1_000_000_000_000_000_000n,
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
const actionParams = actionTracker.createRebalanceAction.lastCall.args[0];
|
|
444
|
+
expect(actionParams.amount).to.equal(1_000_000n);
|
|
445
|
+
});
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it('sendAndConfirmInventoryTx uses the EVM-like signer path for Tron txs', async () => {
|
|
449
|
+
const TRON_CHAIN = 'tron' as ChainName;
|
|
450
|
+
config.inventorySigners[ProtocolType.Tron] = {
|
|
451
|
+
address: INVENTORY_SIGNER,
|
|
452
|
+
key: TEST_PRIVATE_KEY,
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
const getChainMetadata = (chain: ChainName) => ({
|
|
456
|
+
name: chain,
|
|
457
|
+
protocol:
|
|
458
|
+
chain === TRON_CHAIN ? ProtocolType.Tron : ProtocolType.Ethereum,
|
|
459
|
+
blocks: { reorgPeriod: 2 },
|
|
460
|
+
rpcUrls: [{ http: 'http://127.0.0.1:9090/jsonrpc' }],
|
|
461
|
+
});
|
|
462
|
+
warpCore.multiProvider.getChainMetadata.callsFake(getChainMetadata);
|
|
463
|
+
multiProvider.getChainMetadata.callsFake(getChainMetadata);
|
|
464
|
+
warpCore.multiProvider.toMultiProvider =
|
|
465
|
+
Sinon.stub().returns(multiProvider);
|
|
466
|
+
|
|
467
|
+
const sendFn = (inventoryRebalancer as any).sendAndConfirmInventoryTx.bind(
|
|
468
|
+
inventoryRebalancer,
|
|
469
|
+
);
|
|
470
|
+
const result = await sendFn(TRON_CHAIN, {
|
|
471
|
+
category: WarpTxCategory.Transfer,
|
|
472
|
+
type: ProviderType.Tron,
|
|
473
|
+
transaction: {
|
|
474
|
+
to: '0xRouterAddress',
|
|
475
|
+
data: '0xTransferRemoteData',
|
|
476
|
+
value: 1000000n,
|
|
477
|
+
},
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
expect(result).to.deep.equal({ txHash: '0xTransferRemoteTxHash' });
|
|
481
|
+
expect(multiProvider.setSigner.calledOnce).to.be.true;
|
|
482
|
+
expect(
|
|
483
|
+
multiProvider.sendTransaction.calledOnceWithExactly(
|
|
484
|
+
TRON_CHAIN,
|
|
485
|
+
{
|
|
486
|
+
to: '0xRouterAddress',
|
|
487
|
+
data: '0xTransferRemoteData',
|
|
488
|
+
value: 1000000n,
|
|
489
|
+
},
|
|
490
|
+
{ waitConfirmations: 2 },
|
|
491
|
+
),
|
|
492
|
+
).to.be.true;
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
it('buildSignerAccountConfig returns correct config for Tron protocol', () => {
|
|
496
|
+
config.inventorySigners[ProtocolType.Tron] = {
|
|
497
|
+
address: INVENTORY_SIGNER,
|
|
498
|
+
key: TEST_PRIVATE_KEY,
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
const buildFn = (inventoryRebalancer as any).buildSignerAccountConfig.bind(
|
|
502
|
+
inventoryRebalancer,
|
|
503
|
+
);
|
|
504
|
+
const result = buildFn(
|
|
505
|
+
ProtocolType.Tron,
|
|
506
|
+
TEST_PRIVATE_KEY,
|
|
507
|
+
'tron' as ChainName,
|
|
508
|
+
);
|
|
509
|
+
|
|
510
|
+
expect(result.protocol).to.equal(ProtocolType.Tron);
|
|
511
|
+
expect(result.privateKey).to.equal(TEST_PRIVATE_KEY);
|
|
512
|
+
// Tron uses same 0x-prefixed hex key format as Ethereum
|
|
513
|
+
expect(result.privateKey.startsWith('0x')).to.be.true;
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
it('getTransactionReceipt returns EthersV5 type for Tron chain', async () => {
|
|
517
|
+
const TRON_CHAIN = 'tron' as ChainName;
|
|
518
|
+
const mockReceipt = {
|
|
519
|
+
transactionHash: '0xTronReceipt',
|
|
520
|
+
logs: [],
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
warpCore.multiProvider.getChainMetadata.callsFake((chain: ChainName) => ({
|
|
524
|
+
name: chain,
|
|
525
|
+
protocol:
|
|
526
|
+
chain === TRON_CHAIN ? ProtocolType.Tron : ProtocolType.Ethereum,
|
|
527
|
+
}));
|
|
528
|
+
|
|
529
|
+
warpCore.multiProvider.getEthersV5Provider.returns({
|
|
530
|
+
getTransactionReceipt: Sinon.stub().resolves(mockReceipt),
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
const getReceiptFn = (
|
|
534
|
+
inventoryRebalancer as any
|
|
535
|
+
).getTransactionReceipt.bind(inventoryRebalancer);
|
|
536
|
+
const receipt = await getReceiptFn(TRON_CHAIN, '0xTronTxHash');
|
|
537
|
+
|
|
538
|
+
expect(receipt).to.not.be.undefined;
|
|
539
|
+
expect(receipt.type).to.equal(ProviderType.EthersV5);
|
|
540
|
+
expect(receipt.receipt).to.deep.equal(mockReceipt);
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
it('extractDispatchedMessageId returns Tron message ids via the EthersV5 path', async () => {
|
|
544
|
+
const TRON_CHAIN = 'tron' as ChainName;
|
|
545
|
+
const expectedMessageId =
|
|
546
|
+
'0x1111111111111111111111111111111111111111111111111111111111111111';
|
|
547
|
+
const mockReceipt = {
|
|
548
|
+
transactionHash: '0xTronTx',
|
|
549
|
+
logs: [{}],
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
warpCore.multiProvider.getChainMetadata.callsFake((chain: ChainName) => ({
|
|
553
|
+
name: chain,
|
|
554
|
+
protocol:
|
|
555
|
+
chain === TRON_CHAIN ? ProtocolType.Tron : ProtocolType.Ethereum,
|
|
556
|
+
}));
|
|
557
|
+
|
|
558
|
+
const getDispatchedMessagesStub = Sinon.stub(
|
|
559
|
+
HyperlaneCore,
|
|
560
|
+
'getDispatchedMessages',
|
|
561
|
+
).returns([{ id: expectedMessageId }] as any);
|
|
562
|
+
warpCore.multiProvider.getEthersV5Provider.returns({
|
|
563
|
+
getTransactionReceipt: Sinon.stub().resolves(mockReceipt),
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
const extractFn = (
|
|
567
|
+
inventoryRebalancer as any
|
|
568
|
+
).extractDispatchedMessageId.bind(inventoryRebalancer);
|
|
569
|
+
const messageId = await extractFn(TRON_CHAIN, '0xTronTx');
|
|
570
|
+
expect(messageId).to.equal(expectedMessageId);
|
|
571
|
+
expect(getDispatchedMessagesStub.calledOnce).to.be.true;
|
|
572
|
+
expect(getDispatchedMessagesStub.firstCall.firstArg).to.equal(mockReceipt);
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
it('selects the Tron signer address from a mixed multi-protocol config', () => {
|
|
576
|
+
config.inventorySigners[ProtocolType.Tron] = {
|
|
577
|
+
address: INVENTORY_SIGNER,
|
|
578
|
+
key: TEST_PRIVATE_KEY,
|
|
579
|
+
};
|
|
580
|
+
config.inventorySigners[ProtocolType.Sealevel] = {
|
|
581
|
+
address: 'SoLANAAddReSs1234567890123456789012345678',
|
|
582
|
+
key: '1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32',
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
warpCore.multiProvider.getChainMetadata.callsFake((chain: ChainName) => ({
|
|
586
|
+
name: chain,
|
|
587
|
+
protocol:
|
|
588
|
+
chain === 'tron'
|
|
589
|
+
? ProtocolType.Tron
|
|
590
|
+
: chain === SOLANA_CHAIN
|
|
591
|
+
? ProtocolType.Sealevel
|
|
592
|
+
: ProtocolType.Ethereum,
|
|
593
|
+
}));
|
|
594
|
+
|
|
595
|
+
const getInventorySignerAddress = (
|
|
596
|
+
inventoryRebalancer as any
|
|
597
|
+
).getInventorySignerAddress.bind(inventoryRebalancer);
|
|
598
|
+
|
|
599
|
+
expect(getInventorySignerAddress('tron' as ChainName)).to.equal(
|
|
600
|
+
INVENTORY_SIGNER,
|
|
601
|
+
);
|
|
602
|
+
expect(getInventorySignerAddress(SOLANA_CHAIN)).to.equal(
|
|
603
|
+
'SoLANAAddReSs1234567890123456789012345678',
|
|
604
|
+
);
|
|
399
605
|
});
|
|
400
606
|
|
|
401
607
|
describe('Partial Fulfillment (Insufficient Inventory)', () => {
|
|
402
|
-
// Partial transfers happen when maxTransferable >= minViableTransfer
|
|
403
|
-
// For non-native tokens (EvmHypCollateral), minViableTransfer = 0, so
|
|
608
|
+
// Partial transfers happen when maxTransferable >= minViableTransfer.
|
|
609
|
+
// For non-native tokens (EvmHypCollateral), minViableTransfer = 0, so any
|
|
610
|
+
// positive fee-aware maxTransferable remains viable.
|
|
404
611
|
const PARTIAL_AMOUNT = BigInt(5e15); // 0.005 ETH - above threshold
|
|
405
612
|
const FULL_AMOUNT = BigInt(1e16); // 0.01 ETH
|
|
406
613
|
|
|
@@ -433,6 +640,65 @@ describe('InventoryRebalancer E2E', () => {
|
|
|
433
640
|
expect(actionParams.amount).to.equal(PARTIAL_AMOUNT);
|
|
434
641
|
});
|
|
435
642
|
|
|
643
|
+
it('uses fee-aware maxTransferable for non-native token fees', async () => {
|
|
644
|
+
const requestedAmount = 19998000000n;
|
|
645
|
+
const availableInventory = 102466n;
|
|
646
|
+
const safeTransferAmount = 102400n;
|
|
647
|
+
|
|
648
|
+
warpCore.getMaxTransferAmount.resolves({ amount: safeTransferAmount });
|
|
649
|
+
|
|
650
|
+
const route = createTestRoute({ amount: requestedAmount });
|
|
651
|
+
createTestIntent({ amount: requestedAmount });
|
|
652
|
+
|
|
653
|
+
inventoryRebalancer.setInventoryBalances({
|
|
654
|
+
[SOLANA_CHAIN]: availableInventory,
|
|
655
|
+
[ARBITRUM_CHAIN]: 0n,
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
const results = await inventoryRebalancer.rebalance([route]);
|
|
659
|
+
|
|
660
|
+
expect(results).to.have.lengthOf(1);
|
|
661
|
+
expect(results[0].success).to.be.true;
|
|
662
|
+
expect(warpCore.getMaxTransferAmount.calledOnce).to.be.true;
|
|
663
|
+
|
|
664
|
+
const maxTransferArgs = warpCore.getMaxTransferAmount.firstCall.args[0];
|
|
665
|
+
expect(maxTransferArgs.balance.amount).to.equal(availableInventory);
|
|
666
|
+
expect(maxTransferArgs.destination).to.equal(ARBITRUM_CHAIN);
|
|
667
|
+
|
|
668
|
+
const txParams = warpCore.getTransferRemoteTxs.lastCall.args[0];
|
|
669
|
+
expect(txParams.originTokenAmount.amount).to.equal(safeTransferAmount);
|
|
670
|
+
expect(txParams).to.not.have.property('interchainFee');
|
|
671
|
+
expect(txParams).to.not.have.property('tokenFeeQuote');
|
|
672
|
+
|
|
673
|
+
const actionParams = actionTracker.createRebalanceAction.lastCall.args[0];
|
|
674
|
+
expect(actionParams.amount).to.equal(safeTransferAmount);
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
it('downgrades full non-native transfers to partial when fee headroom is missing', async () => {
|
|
678
|
+
const requestedAmount = 102466n;
|
|
679
|
+
const safeTransferAmount = 102400n;
|
|
680
|
+
|
|
681
|
+
warpCore.getMaxTransferAmount.resolves({ amount: safeTransferAmount });
|
|
682
|
+
|
|
683
|
+
const route = createTestRoute({ amount: requestedAmount });
|
|
684
|
+
createTestIntent({ amount: requestedAmount });
|
|
685
|
+
|
|
686
|
+
inventoryRebalancer.setInventoryBalances({
|
|
687
|
+
[SOLANA_CHAIN]: requestedAmount,
|
|
688
|
+
[ARBITRUM_CHAIN]: 0n,
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
const results = await inventoryRebalancer.rebalance([route]);
|
|
692
|
+
|
|
693
|
+
expect(results).to.have.lengthOf(1);
|
|
694
|
+
expect(results[0].success).to.be.true;
|
|
695
|
+
expect(bridge.execute.called).to.be.false;
|
|
696
|
+
|
|
697
|
+
const txParams = warpCore.getTransferRemoteTxs.lastCall.args[0];
|
|
698
|
+
expect(txParams.originTokenAmount.amount).to.equal(safeTransferAmount);
|
|
699
|
+
expect(txParams.originTokenAmount.amount).to.not.equal(requestedAmount);
|
|
700
|
+
});
|
|
701
|
+
|
|
436
702
|
it('intent remains in_progress after partial fulfillment', async () => {
|
|
437
703
|
const route = createTestRoute({ amount: FULL_AMOUNT });
|
|
438
704
|
createTestIntent({ amount: FULL_AMOUNT });
|
|
@@ -450,6 +716,52 @@ describe('InventoryRebalancer E2E', () => {
|
|
|
450
716
|
});
|
|
451
717
|
});
|
|
452
718
|
|
|
719
|
+
describe('Fee-Aware Probe Fallback', () => {
|
|
720
|
+
it('bridges when non-native destination inventory is zero', async () => {
|
|
721
|
+
const requestedAmount = 10000000000n;
|
|
722
|
+
const bufferedBridgeAmount = (requestedAmount * 105n) / 100n;
|
|
723
|
+
const route = createTestRoute({ amount: requestedAmount });
|
|
724
|
+
createTestIntent({ amount: requestedAmount });
|
|
725
|
+
|
|
726
|
+
inventoryRebalancer.setInventoryBalances({
|
|
727
|
+
[SOLANA_CHAIN]: 0n,
|
|
728
|
+
[ARBITRUM_CHAIN]: 20000000000n,
|
|
729
|
+
});
|
|
730
|
+
mockSuccessfulBridge(bufferedBridgeAmount, bufferedBridgeAmount);
|
|
731
|
+
|
|
732
|
+
const results = await inventoryRebalancer.rebalance([route]);
|
|
733
|
+
|
|
734
|
+
expect(results).to.have.lengthOf(1);
|
|
735
|
+
expect(results[0].success).to.be.true;
|
|
736
|
+
expect(warpCore.getMaxTransferAmount.called).to.be.false;
|
|
737
|
+
expect(warpCore.getTransferRemoteTxs.called).to.be.false;
|
|
738
|
+
expect(bridge.execute.calledOnce).to.be.true;
|
|
739
|
+
|
|
740
|
+
const actionParams =
|
|
741
|
+
actionTracker.createRebalanceAction.firstCall.args[0];
|
|
742
|
+
expect(actionParams.type).to.equal('inventory_movement');
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
it('returns failure for unrelated fee-aware probe errors', async () => {
|
|
746
|
+
const route = createTestRoute();
|
|
747
|
+
createTestIntent();
|
|
748
|
+
|
|
749
|
+
inventoryRebalancer.setInventoryBalances({
|
|
750
|
+
[SOLANA_CHAIN]: 1n,
|
|
751
|
+
[ARBITRUM_CHAIN]: 20000000000n,
|
|
752
|
+
});
|
|
753
|
+
warpCore.getMaxTransferAmount.rejects(new Error('RPC down'));
|
|
754
|
+
|
|
755
|
+
const results = await inventoryRebalancer.rebalance([route]);
|
|
756
|
+
|
|
757
|
+
expect(results).to.have.lengthOf(1);
|
|
758
|
+
expect(results[0].success).to.be.false;
|
|
759
|
+
expect(results[0].error).to.include('RPC down');
|
|
760
|
+
expect(bridge.execute.called).to.be.false;
|
|
761
|
+
expect(actionTracker.createRebalanceAction.called).to.be.false;
|
|
762
|
+
});
|
|
763
|
+
});
|
|
764
|
+
|
|
453
765
|
describe('No Inventory Available', () => {
|
|
454
766
|
it('returns failure when no inventory on destination chain and no other source available', async () => {
|
|
455
767
|
const route = createTestRoute();
|
|
@@ -626,6 +938,51 @@ describe('InventoryRebalancer E2E', () => {
|
|
|
626
938
|
// Verify: No new intent was created (existing was continued)
|
|
627
939
|
expect(actionTracker.createRebalanceIntent.called).to.be.false;
|
|
628
940
|
});
|
|
941
|
+
|
|
942
|
+
it('continues existing intent by bridging after recoverable fee-aware probe failure', async () => {
|
|
943
|
+
const existingIntent = createTestIntent({
|
|
944
|
+
id: 'existing-intent',
|
|
945
|
+
status: 'in_progress',
|
|
946
|
+
amount: 10000000000n,
|
|
947
|
+
externalBridge: ExternalBridgeType.LiFi,
|
|
948
|
+
});
|
|
949
|
+
const recoverableProbeError = new Error('Fee probe failed') as Error & {
|
|
950
|
+
cause?: unknown;
|
|
951
|
+
};
|
|
952
|
+
recoverableProbeError.cause = {
|
|
953
|
+
code: 'UNPREDICTABLE_GAS_LIMIT',
|
|
954
|
+
message: 'execution reverted: ERC20: transfer amount exceeds balance',
|
|
955
|
+
};
|
|
956
|
+
|
|
957
|
+
actionTracker.getPartiallyFulfilledInventoryIntents.resolves([
|
|
958
|
+
{
|
|
959
|
+
intent: existingIntent,
|
|
960
|
+
completedAmount: 0n,
|
|
961
|
+
remaining: 10000000000n,
|
|
962
|
+
hasInflightDeposit: false,
|
|
963
|
+
},
|
|
964
|
+
]);
|
|
965
|
+
|
|
966
|
+
inventoryRebalancer.setInventoryBalances({
|
|
967
|
+
[SOLANA_CHAIN]: 1n,
|
|
968
|
+
[ARBITRUM_CHAIN]: 20000000000n,
|
|
969
|
+
});
|
|
970
|
+
warpCore.getMaxTransferAmount.rejects(recoverableProbeError);
|
|
971
|
+
mockSuccessfulBridge(10500000000n, 10500000000n);
|
|
972
|
+
|
|
973
|
+
const results = await inventoryRebalancer.rebalance([
|
|
974
|
+
createTestRoute({ amount: 5000000000n }),
|
|
975
|
+
]);
|
|
976
|
+
|
|
977
|
+
expect(results).to.have.lengthOf(1);
|
|
978
|
+
expect(results[0].success).to.be.true;
|
|
979
|
+
expect(bridge.execute.calledOnce).to.be.true;
|
|
980
|
+
expect(actionTracker.createRebalanceIntent.called).to.be.false;
|
|
981
|
+
|
|
982
|
+
const actionParams =
|
|
983
|
+
actionTracker.createRebalanceAction.firstCall.args[0];
|
|
984
|
+
expect(actionParams.type).to.equal('inventory_movement');
|
|
985
|
+
});
|
|
629
986
|
});
|
|
630
987
|
|
|
631
988
|
describe('Error Handling', () => {
|
|
@@ -1457,6 +1814,83 @@ describe('InventoryRebalancer E2E', () => {
|
|
|
1457
1814
|
expect(bridge.execute.callCount).to.equal(2);
|
|
1458
1815
|
});
|
|
1459
1816
|
|
|
1817
|
+
it('does not inflate each split bridge plan to minViableTransfer', async () => {
|
|
1818
|
+
const amount = 7_000_000_000_000_000n;
|
|
1819
|
+
const perChainInventory = 6_000_000_000_000_000n;
|
|
1820
|
+
const reservedGas = 1_000_000_000n;
|
|
1821
|
+
|
|
1822
|
+
for (const token of warpCore.tokens) {
|
|
1823
|
+
if (
|
|
1824
|
+
token.chainName === ARBITRUM_CHAIN ||
|
|
1825
|
+
token.chainName === SOLANA_CHAIN ||
|
|
1826
|
+
token.chainName === BASE_CHAIN
|
|
1827
|
+
) {
|
|
1828
|
+
token.standard = TokenStandard.EvmHypNative;
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
const route = createTestRoute({ amount });
|
|
1833
|
+
createTestIntent({ amount });
|
|
1834
|
+
|
|
1835
|
+
inventoryRebalancer.setInventoryBalances({
|
|
1836
|
+
[SOLANA_CHAIN]: 0n,
|
|
1837
|
+
[ARBITRUM_CHAIN]: perChainInventory,
|
|
1838
|
+
[BASE_CHAIN]: perChainInventory,
|
|
1839
|
+
});
|
|
1840
|
+
|
|
1841
|
+
bridge.quote.callsFake(async (params: any) =>
|
|
1842
|
+
createMockBridgeQuote({
|
|
1843
|
+
fromAmount: params.fromAmount ?? params.toAmount,
|
|
1844
|
+
toAmount: params.toAmount ?? params.fromAmount,
|
|
1845
|
+
toAmountMin: params.toAmount ?? params.fromAmount,
|
|
1846
|
+
requestParams:
|
|
1847
|
+
params.toAmount !== undefined
|
|
1848
|
+
? { ...params, toAmount: params.toAmount }
|
|
1849
|
+
: { ...params, fromAmount: params.fromAmount },
|
|
1850
|
+
}),
|
|
1851
|
+
);
|
|
1852
|
+
bridge.execute.resolves({
|
|
1853
|
+
txHash: '0xBridgeTxHash',
|
|
1854
|
+
fromChain: 42161,
|
|
1855
|
+
toChain: 1399811149,
|
|
1856
|
+
});
|
|
1857
|
+
|
|
1858
|
+
const results = await inventoryRebalancer.rebalance([route]);
|
|
1859
|
+
|
|
1860
|
+
expect(results).to.have.lengthOf(1);
|
|
1861
|
+
expect(results[0].success).to.be.true;
|
|
1862
|
+
expect(bridge.execute.callCount).to.equal(2);
|
|
1863
|
+
|
|
1864
|
+
const maxPerSourceOutput = perChainInventory - reservedGas;
|
|
1865
|
+
const executionQuoteRequests = bridge.quote
|
|
1866
|
+
.getCalls()
|
|
1867
|
+
.slice(-2)
|
|
1868
|
+
.map((call) => call.args[0]);
|
|
1869
|
+
expect(executionQuoteRequests).to.have.lengthOf(2);
|
|
1870
|
+
|
|
1871
|
+
const forwardQuoteRequests = executionQuoteRequests.filter(
|
|
1872
|
+
(params) => params.fromAmount !== undefined,
|
|
1873
|
+
);
|
|
1874
|
+
const reverseQuoteRequests = executionQuoteRequests.filter(
|
|
1875
|
+
(params) => params.toAmount !== undefined,
|
|
1876
|
+
);
|
|
1877
|
+
const forwardAmounts = forwardQuoteRequests.map(
|
|
1878
|
+
(params) => params.fromAmount as bigint,
|
|
1879
|
+
);
|
|
1880
|
+
|
|
1881
|
+
expect(
|
|
1882
|
+
forwardQuoteRequests.length + reverseQuoteRequests.length,
|
|
1883
|
+
).to.equal(2);
|
|
1884
|
+
expect(forwardQuoteRequests.length).to.be.greaterThan(0);
|
|
1885
|
+
expect(forwardAmounts.every((amount) => amount <= maxPerSourceOutput)).to
|
|
1886
|
+
.be.true;
|
|
1887
|
+
expect(
|
|
1888
|
+
reverseQuoteRequests.every(
|
|
1889
|
+
(params) => (params.toAmount as bigint) <= maxPerSourceOutput,
|
|
1890
|
+
),
|
|
1891
|
+
).to.be.true;
|
|
1892
|
+
});
|
|
1893
|
+
|
|
1460
1894
|
it('applies 5% buffer to total bridge amount', async () => {
|
|
1461
1895
|
// Need 1 ETH -> should plan to bridge 1.05 ETH total (with 5% buffer)
|
|
1462
1896
|
const amount = BigInt(1e18); // 1 ETH
|
|
@@ -1471,14 +1905,19 @@ describe('InventoryRebalancer E2E', () => {
|
|
|
1471
1905
|
[ARBITRUM_CHAIN]: availableInventory,
|
|
1472
1906
|
});
|
|
1473
1907
|
|
|
1474
|
-
|
|
1475
|
-
// (calculateMaxViableBridgeAmount doesn't quote for ERC20 tokens)
|
|
1476
|
-
let quotedFromAmount: bigint | undefined;
|
|
1908
|
+
let quotedTargetOutput: bigint | undefined;
|
|
1477
1909
|
bridge.quote.callsFake(async (params: any) => {
|
|
1478
|
-
|
|
1910
|
+
if (params.toAmount !== undefined) {
|
|
1911
|
+
quotedTargetOutput = params.toAmount;
|
|
1912
|
+
}
|
|
1479
1913
|
return createMockBridgeQuote({
|
|
1480
1914
|
fromAmount: params.fromAmount ?? params.toAmount,
|
|
1481
|
-
toAmount: params.
|
|
1915
|
+
toAmount: params.toAmount ?? params.fromAmount,
|
|
1916
|
+
toAmountMin: params.toAmount ?? params.fromAmount,
|
|
1917
|
+
requestParams:
|
|
1918
|
+
params.toAmount !== undefined
|
|
1919
|
+
? { ...params, toAmount: params.toAmount }
|
|
1920
|
+
: { ...params, fromAmount: params.fromAmount },
|
|
1482
1921
|
});
|
|
1483
1922
|
});
|
|
1484
1923
|
bridge.execute.resolves({
|
|
@@ -1490,10 +1929,465 @@ describe('InventoryRebalancer E2E', () => {
|
|
|
1490
1929
|
await inventoryRebalancer.rebalance([route]);
|
|
1491
1930
|
|
|
1492
1931
|
// Verify: 5% buffer applied (1 ETH * 1.05 = 1.05 ETH)
|
|
1493
|
-
// The bridge plan uses pre-validated amounts (for ERC20, full inventory available)
|
|
1494
|
-
// But the target is (amount * 105%), so if source has >= target, we bridge exactly target
|
|
1495
1932
|
const expectedWithBuffer = (amount * 105n) / 100n;
|
|
1496
|
-
expect(
|
|
1933
|
+
expect(quotedTargetOutput).to.equal(expectedWithBuffer);
|
|
1934
|
+
});
|
|
1935
|
+
|
|
1936
|
+
it('bridges only the mixed-decimal shortfall after dust partial skip', async () => {
|
|
1937
|
+
const canonicalAmount = 1n;
|
|
1938
|
+
const destinationDust = 1_000_000_000_000n - 1n;
|
|
1939
|
+
const sourceInventory = 1_000_000_000_000n;
|
|
1940
|
+
warpCore.tokens.find((t: any) => t.chainName === SOLANA_CHAIN)!.scale = {
|
|
1941
|
+
numerator: 1n,
|
|
1942
|
+
denominator: 1_000_000_000_000n,
|
|
1943
|
+
};
|
|
1944
|
+
|
|
1945
|
+
const route = createTestRoute({ amount: canonicalAmount });
|
|
1946
|
+
createTestIntent({ amount: canonicalAmount });
|
|
1947
|
+
|
|
1948
|
+
inventoryRebalancer.setInventoryBalances({
|
|
1949
|
+
[SOLANA_CHAIN]: destinationDust,
|
|
1950
|
+
[ARBITRUM_CHAIN]: sourceInventory,
|
|
1951
|
+
});
|
|
1952
|
+
|
|
1953
|
+
let reverseQuoteTargetOutput: bigint | undefined;
|
|
1954
|
+
bridge.quote.callsFake(async (params: any) => {
|
|
1955
|
+
if (params.toAmount !== undefined) {
|
|
1956
|
+
reverseQuoteTargetOutput = params.toAmount;
|
|
1957
|
+
}
|
|
1958
|
+
return createMockBridgeQuote({
|
|
1959
|
+
fromAmount: params.fromAmount ?? params.toAmount,
|
|
1960
|
+
toAmount: params.toAmount ?? params.fromAmount,
|
|
1961
|
+
toAmountMin: params.toAmount ?? params.fromAmount,
|
|
1962
|
+
requestParams:
|
|
1963
|
+
params.toAmount !== undefined
|
|
1964
|
+
? { ...params, toAmount: params.toAmount }
|
|
1965
|
+
: { ...params, fromAmount: params.fromAmount },
|
|
1966
|
+
});
|
|
1967
|
+
});
|
|
1968
|
+
bridge.execute.resolves({
|
|
1969
|
+
txHash: '0xBridgeTxHash',
|
|
1970
|
+
fromChain: 42161,
|
|
1971
|
+
toChain: 1399811149,
|
|
1972
|
+
});
|
|
1973
|
+
|
|
1974
|
+
const results = await inventoryRebalancer.rebalance([route]);
|
|
1975
|
+
|
|
1976
|
+
expect(results).to.have.lengthOf(1);
|
|
1977
|
+
expect(results[0].success).to.be.true;
|
|
1978
|
+
expect(warpCore.getTransferRemoteTxs.called).to.be.false;
|
|
1979
|
+
expect(bridge.execute.calledOnce).to.be.true;
|
|
1980
|
+
expect(reverseQuoteTargetOutput).to.equal(1n);
|
|
1981
|
+
});
|
|
1982
|
+
|
|
1983
|
+
it('plans LiFi target output in destination-local units for mixed-decimal routes', async () => {
|
|
1984
|
+
const canonicalAmount = 1_000_000n;
|
|
1985
|
+
const availableInventory = BigInt(2e18);
|
|
1986
|
+
warpCore.tokens.find((t: any) => t.chainName === SOLANA_CHAIN)!.scale = {
|
|
1987
|
+
numerator: 1n,
|
|
1988
|
+
denominator: 1_000_000_000_000n,
|
|
1989
|
+
};
|
|
1990
|
+
|
|
1991
|
+
const route = createTestRoute({ amount: canonicalAmount });
|
|
1992
|
+
createTestIntent({ amount: canonicalAmount });
|
|
1993
|
+
|
|
1994
|
+
inventoryRebalancer.setInventoryBalances({
|
|
1995
|
+
[SOLANA_CHAIN]: 0n,
|
|
1996
|
+
[ARBITRUM_CHAIN]: availableInventory,
|
|
1997
|
+
});
|
|
1998
|
+
|
|
1999
|
+
let quotedTargetOutput: bigint | undefined;
|
|
2000
|
+
bridge.quote.callsFake(async (params: any) => {
|
|
2001
|
+
if (params.toAmount !== undefined) {
|
|
2002
|
+
quotedTargetOutput = params.toAmount;
|
|
2003
|
+
}
|
|
2004
|
+
return createMockBridgeQuote({
|
|
2005
|
+
fromAmount: params.fromAmount ?? params.toAmount,
|
|
2006
|
+
toAmount: params.toAmount ?? params.fromAmount,
|
|
2007
|
+
toAmountMin: params.toAmount ?? params.fromAmount,
|
|
2008
|
+
requestParams:
|
|
2009
|
+
params.toAmount !== undefined
|
|
2010
|
+
? { ...params, toAmount: params.toAmount }
|
|
2011
|
+
: { ...params, fromAmount: params.fromAmount },
|
|
2012
|
+
});
|
|
2013
|
+
});
|
|
2014
|
+
bridge.execute.resolves({
|
|
2015
|
+
txHash: '0xBridgeTxHash',
|
|
2016
|
+
fromChain: 42161,
|
|
2017
|
+
toChain: 1399811149,
|
|
2018
|
+
});
|
|
2019
|
+
|
|
2020
|
+
await inventoryRebalancer.rebalance([route]);
|
|
2021
|
+
|
|
2022
|
+
expect(quotedTargetOutput).to.equal(1_050_000_000_000_000_000n);
|
|
2023
|
+
});
|
|
2024
|
+
|
|
2025
|
+
it('retries forward execution when reverse quote exceeds planned source capacity', async () => {
|
|
2026
|
+
const amount = BigInt(1e18);
|
|
2027
|
+
const sourceInventory = BigInt(2e18);
|
|
2028
|
+
const targetWithBuffer = (amount * 105n) / 100n;
|
|
2029
|
+
|
|
2030
|
+
const route = createTestRoute({ amount });
|
|
2031
|
+
createTestIntent({ amount });
|
|
2032
|
+
|
|
2033
|
+
inventoryRebalancer.setInventoryBalances({
|
|
2034
|
+
[SOLANA_CHAIN]: 0n,
|
|
2035
|
+
[ARBITRUM_CHAIN]: sourceInventory,
|
|
2036
|
+
});
|
|
2037
|
+
|
|
2038
|
+
bridge.quote
|
|
2039
|
+
.onFirstCall()
|
|
2040
|
+
.resolves(
|
|
2041
|
+
createMockBridgeQuote({
|
|
2042
|
+
fromAmount: sourceInventory,
|
|
2043
|
+
toAmount: sourceInventory,
|
|
2044
|
+
toAmountMin: sourceInventory,
|
|
2045
|
+
requestParams: {
|
|
2046
|
+
fromChain: 42161,
|
|
2047
|
+
toChain: 1399811149,
|
|
2048
|
+
fromToken: '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
|
|
2049
|
+
toToken: '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
|
|
2050
|
+
fromAddress: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
2051
|
+
toAddress: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
2052
|
+
fromAmount: sourceInventory,
|
|
2053
|
+
},
|
|
2054
|
+
}),
|
|
2055
|
+
)
|
|
2056
|
+
.onSecondCall()
|
|
2057
|
+
.resolves(
|
|
2058
|
+
createMockBridgeQuote({
|
|
2059
|
+
fromAmount: sourceInventory + 1n,
|
|
2060
|
+
toAmount: targetWithBuffer,
|
|
2061
|
+
toAmountMin: targetWithBuffer,
|
|
2062
|
+
requestParams: {
|
|
2063
|
+
fromChain: 42161,
|
|
2064
|
+
toChain: 1399811149,
|
|
2065
|
+
fromToken: '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
|
|
2066
|
+
toToken: '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
|
|
2067
|
+
fromAddress: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
2068
|
+
toAddress: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
2069
|
+
toAmount: targetWithBuffer,
|
|
2070
|
+
},
|
|
2071
|
+
}),
|
|
2072
|
+
)
|
|
2073
|
+
.onThirdCall()
|
|
2074
|
+
.resolves(
|
|
2075
|
+
createMockBridgeQuote({
|
|
2076
|
+
fromAmount: sourceInventory,
|
|
2077
|
+
toAmount: targetWithBuffer - 1n,
|
|
2078
|
+
toAmountMin: targetWithBuffer - 1n,
|
|
2079
|
+
requestParams: {
|
|
2080
|
+
fromChain: 42161,
|
|
2081
|
+
toChain: 1399811149,
|
|
2082
|
+
fromToken: '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
|
|
2083
|
+
toToken: '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
|
|
2084
|
+
fromAddress: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
2085
|
+
toAddress: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
2086
|
+
fromAmount: sourceInventory,
|
|
2087
|
+
},
|
|
2088
|
+
}),
|
|
2089
|
+
);
|
|
2090
|
+
|
|
2091
|
+
bridge.execute.resolves({
|
|
2092
|
+
txHash: '0xBridgeTxHash',
|
|
2093
|
+
fromChain: 42161,
|
|
2094
|
+
toChain: 1399811149,
|
|
2095
|
+
});
|
|
2096
|
+
|
|
2097
|
+
const results = await inventoryRebalancer.rebalance([route]);
|
|
2098
|
+
|
|
2099
|
+
expect(results).to.have.lengthOf(1);
|
|
2100
|
+
expect(results[0].success).to.be.true;
|
|
2101
|
+
expect(bridge.execute.calledOnce).to.be.true;
|
|
2102
|
+
expect(bridge.quote.callCount).to.equal(3);
|
|
2103
|
+
expect(bridge.quote.getCall(1).args[0].toAmount).to.equal(
|
|
2104
|
+
targetWithBuffer,
|
|
2105
|
+
);
|
|
2106
|
+
expect(bridge.quote.getCall(2).args[0].fromAmount).to.equal(
|
|
2107
|
+
sourceInventory,
|
|
2108
|
+
);
|
|
2109
|
+
expect(bridge.quote.getCall(2).args[0].toAmount).to.be.undefined;
|
|
2110
|
+
});
|
|
2111
|
+
|
|
2112
|
+
it('fails when forward retry still exceeds planned source capacity', async () => {
|
|
2113
|
+
const amount = BigInt(1e18);
|
|
2114
|
+
const sourceInventory = BigInt(2e18);
|
|
2115
|
+
const targetWithBuffer = (amount * 105n) / 100n;
|
|
2116
|
+
|
|
2117
|
+
const route = createTestRoute({ amount });
|
|
2118
|
+
createTestIntent({ amount });
|
|
2119
|
+
|
|
2120
|
+
inventoryRebalancer.setInventoryBalances({
|
|
2121
|
+
[SOLANA_CHAIN]: 0n,
|
|
2122
|
+
[ARBITRUM_CHAIN]: sourceInventory,
|
|
2123
|
+
});
|
|
2124
|
+
|
|
2125
|
+
bridge.quote
|
|
2126
|
+
.onFirstCall()
|
|
2127
|
+
.resolves(
|
|
2128
|
+
createMockBridgeQuote({
|
|
2129
|
+
fromAmount: sourceInventory,
|
|
2130
|
+
toAmount: sourceInventory,
|
|
2131
|
+
toAmountMin: sourceInventory,
|
|
2132
|
+
requestParams: {
|
|
2133
|
+
fromChain: 42161,
|
|
2134
|
+
toChain: 1399811149,
|
|
2135
|
+
fromToken: '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
|
|
2136
|
+
toToken: '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
|
|
2137
|
+
fromAddress: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
2138
|
+
toAddress: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
2139
|
+
fromAmount: sourceInventory,
|
|
2140
|
+
},
|
|
2141
|
+
}),
|
|
2142
|
+
)
|
|
2143
|
+
.onSecondCall()
|
|
2144
|
+
.resolves(
|
|
2145
|
+
createMockBridgeQuote({
|
|
2146
|
+
fromAmount: sourceInventory + 1n,
|
|
2147
|
+
toAmount: targetWithBuffer,
|
|
2148
|
+
toAmountMin: targetWithBuffer,
|
|
2149
|
+
requestParams: {
|
|
2150
|
+
fromChain: 42161,
|
|
2151
|
+
toChain: 1399811149,
|
|
2152
|
+
fromToken: '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
|
|
2153
|
+
toToken: '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
|
|
2154
|
+
fromAddress: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
2155
|
+
toAddress: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
2156
|
+
toAmount: targetWithBuffer,
|
|
2157
|
+
},
|
|
2158
|
+
}),
|
|
2159
|
+
)
|
|
2160
|
+
.onThirdCall()
|
|
2161
|
+
.resolves(
|
|
2162
|
+
createMockBridgeQuote({
|
|
2163
|
+
fromAmount: sourceInventory + 1n,
|
|
2164
|
+
toAmount: targetWithBuffer - 1n,
|
|
2165
|
+
toAmountMin: targetWithBuffer - 1n,
|
|
2166
|
+
requestParams: {
|
|
2167
|
+
fromChain: 42161,
|
|
2168
|
+
toChain: 1399811149,
|
|
2169
|
+
fromToken: '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
|
|
2170
|
+
toToken: '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
|
|
2171
|
+
fromAddress: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
2172
|
+
toAddress: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
2173
|
+
fromAmount: sourceInventory,
|
|
2174
|
+
},
|
|
2175
|
+
}),
|
|
2176
|
+
);
|
|
2177
|
+
|
|
2178
|
+
const results = await inventoryRebalancer.rebalance([route]);
|
|
2179
|
+
|
|
2180
|
+
expect(results).to.have.lengthOf(1);
|
|
2181
|
+
expect(results[0].success).to.be.false;
|
|
2182
|
+
expect(results[0].error).to.include('All inventory movements failed');
|
|
2183
|
+
expect(results[0].error).to.include('exceeded planned source capacity');
|
|
2184
|
+
expect(bridge.quote.callCount).to.equal(3);
|
|
2185
|
+
expect(bridge.execute.called).to.be.false;
|
|
2186
|
+
expect(bridge.quote.getCall(1).args[0].toAmount).to.equal(
|
|
2187
|
+
targetWithBuffer,
|
|
2188
|
+
);
|
|
2189
|
+
expect(bridge.quote.getCall(2).args[0].fromAmount).to.equal(
|
|
2190
|
+
sourceInventory,
|
|
2191
|
+
);
|
|
2192
|
+
expect(bridge.quote.getCall(2).args[0].toAmount).to.be.undefined;
|
|
2193
|
+
});
|
|
2194
|
+
|
|
2195
|
+
it('fails gracefully when execute-time bridge quote rejects', async () => {
|
|
2196
|
+
const amount = BigInt(1e18);
|
|
2197
|
+
const sourceInventory = BigInt(2e18);
|
|
2198
|
+
|
|
2199
|
+
const route = createTestRoute({ amount });
|
|
2200
|
+
createTestIntent({ amount });
|
|
2201
|
+
|
|
2202
|
+
inventoryRebalancer.setInventoryBalances({
|
|
2203
|
+
[SOLANA_CHAIN]: 0n,
|
|
2204
|
+
[ARBITRUM_CHAIN]: sourceInventory,
|
|
2205
|
+
});
|
|
2206
|
+
|
|
2207
|
+
bridge.quote
|
|
2208
|
+
.onFirstCall()
|
|
2209
|
+
.resolves(
|
|
2210
|
+
createMockBridgeQuote({
|
|
2211
|
+
fromAmount: sourceInventory,
|
|
2212
|
+
toAmount: sourceInventory,
|
|
2213
|
+
toAmountMin: sourceInventory,
|
|
2214
|
+
requestParams: {
|
|
2215
|
+
fromChain: 42161,
|
|
2216
|
+
toChain: 1399811149,
|
|
2217
|
+
fromToken: '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
|
|
2218
|
+
toToken: '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
|
|
2219
|
+
fromAddress: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
2220
|
+
toAddress: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
2221
|
+
fromAmount: sourceInventory,
|
|
2222
|
+
},
|
|
2223
|
+
}),
|
|
2224
|
+
)
|
|
2225
|
+
.onSecondCall()
|
|
2226
|
+
.rejects(new Error('LiFi API timeout'));
|
|
2227
|
+
|
|
2228
|
+
const results = await inventoryRebalancer.rebalance([route]);
|
|
2229
|
+
|
|
2230
|
+
expect(results).to.have.lengthOf(1);
|
|
2231
|
+
expect(results[0].success).to.be.false;
|
|
2232
|
+
expect(results[0].error).to.include('All inventory movements failed');
|
|
2233
|
+
expect(results[0].error).to.include('LiFi API timeout');
|
|
2234
|
+
expect(bridge.quote.callCount).to.equal(2);
|
|
2235
|
+
expect(bridge.execute.called).to.be.false;
|
|
2236
|
+
});
|
|
2237
|
+
|
|
2238
|
+
it('preserves non-Error rejection reasons from parallel bridge execution', async () => {
|
|
2239
|
+
const amount = BigInt(1e18);
|
|
2240
|
+
const sourceInventory = BigInt(2e18);
|
|
2241
|
+
|
|
2242
|
+
const route = createTestRoute({ amount });
|
|
2243
|
+
createTestIntent({ amount });
|
|
2244
|
+
|
|
2245
|
+
inventoryRebalancer.setInventoryBalances({
|
|
2246
|
+
[SOLANA_CHAIN]: 0n,
|
|
2247
|
+
[ARBITRUM_CHAIN]: sourceInventory,
|
|
2248
|
+
});
|
|
2249
|
+
|
|
2250
|
+
bridge.quote.onFirstCall().resolves(
|
|
2251
|
+
createMockBridgeQuote({
|
|
2252
|
+
fromAmount: sourceInventory,
|
|
2253
|
+
toAmount: sourceInventory,
|
|
2254
|
+
toAmountMin: sourceInventory,
|
|
2255
|
+
requestParams: {
|
|
2256
|
+
fromChain: 42161,
|
|
2257
|
+
toChain: 1399811149,
|
|
2258
|
+
fromToken: '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
|
|
2259
|
+
toToken: '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
|
|
2260
|
+
fromAddress: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
2261
|
+
toAddress: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
2262
|
+
fromAmount: sourceInventory,
|
|
2263
|
+
},
|
|
2264
|
+
}),
|
|
2265
|
+
);
|
|
2266
|
+
|
|
2267
|
+
// Simulate a foreign promise rejecting with a non-Error reason at runtime.
|
|
2268
|
+
const rejectionReason = 'bridge exploded' as unknown as Error;
|
|
2269
|
+
|
|
2270
|
+
const executeInventoryMovementStub = Sinon.stub(
|
|
2271
|
+
inventoryRebalancer as unknown as {
|
|
2272
|
+
executeInventoryMovement: () => Promise<unknown>;
|
|
2273
|
+
},
|
|
2274
|
+
'executeInventoryMovement',
|
|
2275
|
+
).callsFake(() =>
|
|
2276
|
+
Promise.resolve().then(() => {
|
|
2277
|
+
throw rejectionReason;
|
|
2278
|
+
}),
|
|
2279
|
+
);
|
|
2280
|
+
|
|
2281
|
+
const results = await inventoryRebalancer.rebalance([route]);
|
|
2282
|
+
|
|
2283
|
+
expect(results).to.have.lengthOf(1);
|
|
2284
|
+
expect(results[0].success).to.be.false;
|
|
2285
|
+
expect(results[0].error).to.include('All inventory movements failed');
|
|
2286
|
+
expect(results[0].error).to.include('arbitrum: bridge exploded');
|
|
2287
|
+
expect(executeInventoryMovementStub.calledOnce).to.be.true;
|
|
2288
|
+
expect(bridge.execute.called).to.be.false;
|
|
2289
|
+
|
|
2290
|
+
executeInventoryMovementStub.restore();
|
|
2291
|
+
});
|
|
2292
|
+
|
|
2293
|
+
it('logs actual successful bridge output floors instead of planned output totals', async () => {
|
|
2294
|
+
const amount = BigInt(1e18);
|
|
2295
|
+
const perChainInventory = BigInt(0.6e18);
|
|
2296
|
+
const logger = {
|
|
2297
|
+
info: Sinon.stub(),
|
|
2298
|
+
debug: Sinon.stub(),
|
|
2299
|
+
warn: Sinon.stub(),
|
|
2300
|
+
error: Sinon.stub(),
|
|
2301
|
+
};
|
|
2302
|
+
|
|
2303
|
+
const localRebalancer = new InventoryRebalancer(
|
|
2304
|
+
config,
|
|
2305
|
+
actionTracker as unknown as IActionTracker,
|
|
2306
|
+
{ lifi: bridge as unknown as IExternalBridge },
|
|
2307
|
+
warpCore as unknown as WarpCore,
|
|
2308
|
+
multiProvider as unknown as MultiProvider,
|
|
2309
|
+
logger as unknown as Logger,
|
|
2310
|
+
);
|
|
2311
|
+
|
|
2312
|
+
const route = createTestRoute({ amount });
|
|
2313
|
+
createTestIntent({ amount });
|
|
2314
|
+
|
|
2315
|
+
localRebalancer.setInventoryBalances({
|
|
2316
|
+
[SOLANA_CHAIN]: 0n,
|
|
2317
|
+
[ARBITRUM_CHAIN]: perChainInventory,
|
|
2318
|
+
[BASE_CHAIN]: perChainInventory,
|
|
2319
|
+
});
|
|
2320
|
+
|
|
2321
|
+
bridge.quote
|
|
2322
|
+
.onCall(0)
|
|
2323
|
+
.resolves(
|
|
2324
|
+
createMockBridgeQuote({
|
|
2325
|
+
fromAmount: perChainInventory,
|
|
2326
|
+
toAmount: BigInt(0.525e18),
|
|
2327
|
+
toAmountMin: BigInt(0.525e18),
|
|
2328
|
+
}),
|
|
2329
|
+
)
|
|
2330
|
+
.onCall(1)
|
|
2331
|
+
.resolves(
|
|
2332
|
+
createMockBridgeQuote({
|
|
2333
|
+
fromAmount: perChainInventory,
|
|
2334
|
+
toAmount: BigInt(0.525e18),
|
|
2335
|
+
toAmountMin: BigInt(0.525e18),
|
|
2336
|
+
}),
|
|
2337
|
+
)
|
|
2338
|
+
.onCall(2)
|
|
2339
|
+
.resolves(
|
|
2340
|
+
createMockBridgeQuote({
|
|
2341
|
+
fromAmount: perChainInventory,
|
|
2342
|
+
toAmount: BigInt(0.505e18),
|
|
2343
|
+
toAmountMin: BigInt(0.5e18),
|
|
2344
|
+
}),
|
|
2345
|
+
)
|
|
2346
|
+
.onCall(3)
|
|
2347
|
+
.resolves(
|
|
2348
|
+
createMockBridgeQuote({
|
|
2349
|
+
fromAmount: perChainInventory,
|
|
2350
|
+
toAmount: BigInt(0.515e18),
|
|
2351
|
+
toAmountMin: BigInt(0.51e18),
|
|
2352
|
+
}),
|
|
2353
|
+
);
|
|
2354
|
+
|
|
2355
|
+
bridge.execute
|
|
2356
|
+
.onFirstCall()
|
|
2357
|
+
.resolves({
|
|
2358
|
+
txHash: '0xSuccessTxHash1',
|
|
2359
|
+
fromChain: 42161,
|
|
2360
|
+
toChain: 1399811149,
|
|
2361
|
+
})
|
|
2362
|
+
.onSecondCall()
|
|
2363
|
+
.resolves({
|
|
2364
|
+
txHash: '0xSuccessTxHash2',
|
|
2365
|
+
fromChain: 8453,
|
|
2366
|
+
toChain: 1399811149,
|
|
2367
|
+
});
|
|
2368
|
+
|
|
2369
|
+
const results = await localRebalancer.rebalance([route]);
|
|
2370
|
+
|
|
2371
|
+
expect(results).to.have.lengthOf(1);
|
|
2372
|
+
expect(results[0].success).to.be.true;
|
|
2373
|
+
expect(bridge.execute.callCount).to.equal(2);
|
|
2374
|
+
|
|
2375
|
+
const summaryCall = logger.info
|
|
2376
|
+
.getCalls()
|
|
2377
|
+
.find(
|
|
2378
|
+
(call: ReturnType<typeof logger.info.getCall>) =>
|
|
2379
|
+
call.args[0] &&
|
|
2380
|
+
typeof call.args[0] === 'object' &&
|
|
2381
|
+
Object.prototype.hasOwnProperty.call(
|
|
2382
|
+
call.args[0],
|
|
2383
|
+
'totalQuotedOutputMin',
|
|
2384
|
+
),
|
|
2385
|
+
);
|
|
2386
|
+
|
|
2387
|
+
assert(summaryCall, 'Expected summary log with totalQuotedOutputMin');
|
|
2388
|
+
expect(summaryCall.args[0].totalQuotedOutputMin).to.equal(
|
|
2389
|
+
BigInt(1.01e18).toString(),
|
|
2390
|
+
);
|
|
1497
2391
|
});
|
|
1498
2392
|
|
|
1499
2393
|
it('continues when some bridges fail', async () => {
|
|
@@ -1721,14 +2615,36 @@ describe('InventoryRebalancer E2E', () => {
|
|
|
1721
2615
|
});
|
|
1722
2616
|
|
|
1723
2617
|
// Quote with high gas costs (but for ERC20, this shouldn't trigger viability check)
|
|
1724
|
-
bridge.quote
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
2618
|
+
bridge.quote
|
|
2619
|
+
.onFirstCall()
|
|
2620
|
+
.resolves(
|
|
2621
|
+
createMockBridgeQuote({
|
|
2622
|
+
fromAmount: tokenBalance,
|
|
2623
|
+
toAmount: tokenBalance,
|
|
2624
|
+
toAmountMin: tokenBalance,
|
|
2625
|
+
gasCosts: BigInt(1e18), // Very high gas (would fail viability if this were native)
|
|
2626
|
+
feeCosts: 0n,
|
|
2627
|
+
}),
|
|
2628
|
+
)
|
|
2629
|
+
.onSecondCall()
|
|
2630
|
+
.resolves(
|
|
2631
|
+
createMockBridgeQuote({
|
|
2632
|
+
fromAmount: tokenBalance,
|
|
2633
|
+
toAmount: tokenBalance,
|
|
2634
|
+
toAmountMin: tokenBalance,
|
|
2635
|
+
gasCosts: BigInt(1e18),
|
|
2636
|
+
feeCosts: 0n,
|
|
2637
|
+
requestParams: {
|
|
2638
|
+
fromChain: 42161,
|
|
2639
|
+
toChain: 1399811149,
|
|
2640
|
+
fromToken: '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
|
|
2641
|
+
toToken: '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
|
|
2642
|
+
fromAddress: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
2643
|
+
toAddress: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
2644
|
+
toAmount: tokenBalance,
|
|
2645
|
+
},
|
|
2646
|
+
}),
|
|
2647
|
+
);
|
|
1732
2648
|
|
|
1733
2649
|
bridge.execute.resolves({
|
|
1734
2650
|
txHash: '0xERC20BridgeTxHash',
|
|
@@ -1742,6 +2658,9 @@ describe('InventoryRebalancer E2E', () => {
|
|
|
1742
2658
|
expect(results).to.have.lengthOf(1);
|
|
1743
2659
|
expect(results[0].success).to.be.true;
|
|
1744
2660
|
expect(bridge.execute.calledOnce).to.be.true;
|
|
2661
|
+
expect(bridge.quote.callCount).to.equal(2);
|
|
2662
|
+
expect(bridge.quote.getCall(1).args[0].fromAmount).to.equal(tokenBalance);
|
|
2663
|
+
expect(bridge.quote.getCall(1).args[0].toAmount).to.be.undefined;
|
|
1745
2664
|
});
|
|
1746
2665
|
|
|
1747
2666
|
it('calculates max viable amount as inventory minus (gasCosts * 20) for native tokens', async () => {
|
|
@@ -1777,14 +2696,47 @@ describe('InventoryRebalancer E2E', () => {
|
|
|
1777
2696
|
// maxViable = 1 ETH - 0.02 ETH = 0.98 ETH
|
|
1778
2697
|
// 10% threshold = 0.1 ETH, estimatedGas (0.02) < threshold → viable
|
|
1779
2698
|
const gasCosts = BigInt(0.001e18); // 0.001 ETH
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
2699
|
+
const maxViable = rawBalance - gasCosts * 20n;
|
|
2700
|
+
bridge.quote
|
|
2701
|
+
.onFirstCall()
|
|
2702
|
+
.resolves(
|
|
2703
|
+
createMockBridgeQuote({
|
|
2704
|
+
fromAmount: rawBalance,
|
|
2705
|
+
toAmount: rawBalance - BigInt(1e15),
|
|
2706
|
+
toAmountMin: rawBalance - BigInt(1e15),
|
|
2707
|
+
gasCosts,
|
|
2708
|
+
feeCosts: 0n,
|
|
2709
|
+
}),
|
|
2710
|
+
)
|
|
2711
|
+
.onSecondCall()
|
|
2712
|
+
.resolves(
|
|
2713
|
+
createMockBridgeQuote({
|
|
2714
|
+
fromAmount: maxViable,
|
|
2715
|
+
toAmount: maxViable - BigInt(1e15),
|
|
2716
|
+
toAmountMin: maxViable - BigInt(1e15),
|
|
2717
|
+
gasCosts,
|
|
2718
|
+
feeCosts: 0n,
|
|
2719
|
+
}),
|
|
2720
|
+
)
|
|
2721
|
+
.onThirdCall()
|
|
2722
|
+
.resolves(
|
|
2723
|
+
createMockBridgeQuote({
|
|
2724
|
+
fromAmount: BigInt(0.525e18),
|
|
2725
|
+
toAmount: BigInt(0.525e18),
|
|
2726
|
+
toAmountMin: BigInt(0.525e18),
|
|
2727
|
+
gasCosts,
|
|
2728
|
+
feeCosts: 0n,
|
|
2729
|
+
requestParams: {
|
|
2730
|
+
fromChain: 42161,
|
|
2731
|
+
toChain: 1399811149,
|
|
2732
|
+
fromToken: '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
|
|
2733
|
+
toToken: '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
|
|
2734
|
+
fromAddress: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
2735
|
+
toAddress: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
2736
|
+
toAmount: BigInt(0.525e18),
|
|
2737
|
+
},
|
|
2738
|
+
}),
|
|
2739
|
+
);
|
|
1788
2740
|
|
|
1789
2741
|
bridge.execute.resolves({
|
|
1790
2742
|
txHash: '0xSuccessBridgeTxHash',
|
|
@@ -1798,19 +2750,121 @@ describe('InventoryRebalancer E2E', () => {
|
|
|
1798
2750
|
expect(results).to.have.lengthOf(1);
|
|
1799
2751
|
expect(results[0].success).to.be.true;
|
|
1800
2752
|
expect(bridge.execute.calledOnce).to.be.true;
|
|
2753
|
+
expect(bridge.quote.callCount).to.equal(3);
|
|
2754
|
+
|
|
2755
|
+
expect(bridge.quote.getCall(1).args[0].fromAmount).to.equal(maxViable);
|
|
2756
|
+
const executionTargetOutput = bridge.quote.getCall(2).args[0].toAmount;
|
|
2757
|
+
expect(executionTargetOutput).to.be.a('bigint');
|
|
2758
|
+
if (executionTargetOutput === undefined) {
|
|
2759
|
+
throw new Error('Expected reverse quote to set toAmount');
|
|
2760
|
+
}
|
|
2761
|
+
expect(executionTargetOutput > amount).to.be.true;
|
|
2762
|
+
expect(executionTargetOutput <= maxViable).to.be.true;
|
|
2763
|
+
});
|
|
1801
2764
|
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
const
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
2765
|
+
it('uses forward quote when target output exactly matches source capacity', async () => {
|
|
2766
|
+
const amount = 1000n;
|
|
2767
|
+
const rawBalance = 1100n;
|
|
2768
|
+
const targetWithBuffer = 1050n;
|
|
2769
|
+
|
|
2770
|
+
const route = createTestRoute({ amount });
|
|
2771
|
+
createTestIntent({ amount });
|
|
2772
|
+
|
|
2773
|
+
inventoryRebalancer.setInventoryBalances({
|
|
2774
|
+
[SOLANA_CHAIN]: 0n,
|
|
2775
|
+
[ARBITRUM_CHAIN]: rawBalance,
|
|
2776
|
+
});
|
|
2777
|
+
|
|
2778
|
+
bridge.quote
|
|
2779
|
+
.onFirstCall()
|
|
2780
|
+
.resolves(
|
|
2781
|
+
createMockBridgeQuote({
|
|
2782
|
+
fromAmount: rawBalance,
|
|
2783
|
+
toAmount: targetWithBuffer,
|
|
2784
|
+
toAmountMin: targetWithBuffer,
|
|
2785
|
+
}),
|
|
2786
|
+
)
|
|
2787
|
+
.onSecondCall()
|
|
2788
|
+
.resolves(
|
|
2789
|
+
createMockBridgeQuote({
|
|
2790
|
+
fromAmount: rawBalance,
|
|
2791
|
+
toAmount: targetWithBuffer,
|
|
2792
|
+
toAmountMin: targetWithBuffer,
|
|
2793
|
+
}),
|
|
1811
2794
|
);
|
|
1812
|
-
|
|
1813
|
-
|
|
2795
|
+
|
|
2796
|
+
bridge.execute.resolves({
|
|
2797
|
+
txHash: '0xForwardBoundaryBridgeTxHash',
|
|
2798
|
+
fromChain: 42161,
|
|
2799
|
+
toChain: 1399811149,
|
|
2800
|
+
});
|
|
2801
|
+
|
|
2802
|
+
const results = await inventoryRebalancer.rebalance([route]);
|
|
2803
|
+
|
|
2804
|
+
expect(results).to.have.lengthOf(1);
|
|
2805
|
+
expect(results[0].success).to.be.true;
|
|
2806
|
+
expect(bridge.execute.calledOnce).to.be.true;
|
|
2807
|
+
expect(bridge.quote.callCount).to.equal(2);
|
|
2808
|
+
expect(bridge.quote.getCall(1).args[0].fromAmount).to.equal(rawBalance);
|
|
2809
|
+
expect(bridge.quote.getCall(1).args[0].toAmount).to.be.undefined;
|
|
2810
|
+
});
|
|
2811
|
+
|
|
2812
|
+
it('retries reverse quote as forward when exact-output exceeds source capacity', async () => {
|
|
2813
|
+
const amount = 1000n;
|
|
2814
|
+
const rawBalance = 1100n;
|
|
2815
|
+
const targetWithBuffer = 1050n;
|
|
2816
|
+
|
|
2817
|
+
const route = createTestRoute({ amount });
|
|
2818
|
+
createTestIntent({ amount });
|
|
2819
|
+
|
|
2820
|
+
inventoryRebalancer.setInventoryBalances({
|
|
2821
|
+
[SOLANA_CHAIN]: 0n,
|
|
2822
|
+
[ARBITRUM_CHAIN]: rawBalance,
|
|
2823
|
+
});
|
|
2824
|
+
|
|
2825
|
+
bridge.quote
|
|
2826
|
+
.onFirstCall()
|
|
2827
|
+
.resolves(
|
|
2828
|
+
createMockBridgeQuote({
|
|
2829
|
+
fromAmount: rawBalance,
|
|
2830
|
+
toAmount: 1051n,
|
|
2831
|
+
toAmountMin: 1051n,
|
|
2832
|
+
}),
|
|
2833
|
+
)
|
|
2834
|
+
.onSecondCall()
|
|
2835
|
+
.resolves(
|
|
2836
|
+
createMockBridgeQuote({
|
|
2837
|
+
fromAmount: rawBalance + 1n,
|
|
2838
|
+
toAmount: targetWithBuffer,
|
|
2839
|
+
toAmountMin: targetWithBuffer,
|
|
2840
|
+
}),
|
|
2841
|
+
)
|
|
2842
|
+
.onThirdCall()
|
|
2843
|
+
.resolves(
|
|
2844
|
+
createMockBridgeQuote({
|
|
2845
|
+
fromAmount: rawBalance,
|
|
2846
|
+
toAmount: 1049n,
|
|
2847
|
+
toAmountMin: 1049n,
|
|
2848
|
+
}),
|
|
2849
|
+
);
|
|
2850
|
+
|
|
2851
|
+
bridge.execute.resolves({
|
|
2852
|
+
txHash: '0xForwardFallbackBridgeTxHash',
|
|
2853
|
+
fromChain: 42161,
|
|
2854
|
+
toChain: 1399811149,
|
|
2855
|
+
});
|
|
2856
|
+
|
|
2857
|
+
const results = await inventoryRebalancer.rebalance([route]);
|
|
2858
|
+
|
|
2859
|
+
expect(results).to.have.lengthOf(1);
|
|
2860
|
+
expect(results[0].success).to.be.true;
|
|
2861
|
+
expect(bridge.execute.calledOnce).to.be.true;
|
|
2862
|
+
expect(bridge.quote.callCount).to.equal(3);
|
|
2863
|
+
expect(bridge.quote.getCall(1).args[0].toAmount).to.equal(
|
|
2864
|
+
targetWithBuffer,
|
|
2865
|
+
);
|
|
2866
|
+
expect(bridge.quote.getCall(2).args[0].fromAmount).to.equal(rawBalance);
|
|
2867
|
+
expect(bridge.quote.getCall(2).args[0].toAmount).to.be.undefined;
|
|
1814
2868
|
});
|
|
1815
2869
|
|
|
1816
2870
|
it('handles quote failures gracefully by skipping the source chain', async () => {
|