@hyperlane-xyz/rebalancer 27.2.13 → 27.2.14
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.js +1 -1
- package/dist/bridges/LiFiBridge.js.map +1 -1
- package/dist/bridges/LiFiBridge.test.js +37 -0
- package/dist/bridges/LiFiBridge.test.js.map +1 -1
- package/dist/core/InventoryRebalancer.d.ts +4 -2
- package/dist/core/InventoryRebalancer.d.ts.map +1 -1
- package/dist/core/InventoryRebalancer.js +81 -23
- package/dist/core/InventoryRebalancer.js.map +1 -1
- package/dist/core/InventoryRebalancer.test.js +328 -20
- package/dist/core/InventoryRebalancer.test.js.map +1 -1
- package/package.json +7 -7
- package/src/bridges/LiFiBridge.test.ts +43 -0
- package/src/bridges/LiFiBridge.ts +1 -1
- package/src/core/InventoryRebalancer.test.ts +450 -21
- package/src/core/InventoryRebalancer.ts +113 -28
|
@@ -1,6 +1,7 @@
|
|
|
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
|
|
|
@@ -12,7 +13,7 @@ import {
|
|
|
12
13
|
WarpTxCategory,
|
|
13
14
|
type WarpCore,
|
|
14
15
|
} from '@hyperlane-xyz/sdk';
|
|
15
|
-
import { ProtocolType } from '@hyperlane-xyz/utils';
|
|
16
|
+
import { assert, ProtocolType } from '@hyperlane-xyz/utils';
|
|
16
17
|
|
|
17
18
|
import { ExternalBridgeType } from '../config/types.js';
|
|
18
19
|
import type { IExternalBridge } from '../interfaces/IExternalBridge.js';
|
|
@@ -1700,24 +1701,34 @@ describe('InventoryRebalancer E2E', () => {
|
|
|
1700
1701
|
expect(results[0].success).to.be.true;
|
|
1701
1702
|
expect(bridge.execute.callCount).to.equal(2);
|
|
1702
1703
|
|
|
1703
|
-
const
|
|
1704
|
+
const maxPerSourceOutput = perChainInventory - reservedGas;
|
|
1705
|
+
const executionQuoteRequests = bridge.quote
|
|
1704
1706
|
.getCalls()
|
|
1705
|
-
.
|
|
1706
|
-
.
|
|
1707
|
-
expect(
|
|
1707
|
+
.slice(-2)
|
|
1708
|
+
.map((call) => call.args[0]);
|
|
1709
|
+
expect(executionQuoteRequests).to.have.lengthOf(2);
|
|
1708
1710
|
|
|
1709
|
-
const
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
const
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
.
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
expect(
|
|
1720
|
-
.
|
|
1711
|
+
const forwardQuoteRequests = executionQuoteRequests.filter(
|
|
1712
|
+
(params) => params.fromAmount !== undefined,
|
|
1713
|
+
);
|
|
1714
|
+
const reverseQuoteRequests = executionQuoteRequests.filter(
|
|
1715
|
+
(params) => params.toAmount !== undefined,
|
|
1716
|
+
);
|
|
1717
|
+
const forwardAmounts = forwardQuoteRequests.map(
|
|
1718
|
+
(params) => params.fromAmount as bigint,
|
|
1719
|
+
);
|
|
1720
|
+
|
|
1721
|
+
expect(
|
|
1722
|
+
forwardQuoteRequests.length + reverseQuoteRequests.length,
|
|
1723
|
+
).to.equal(2);
|
|
1724
|
+
expect(forwardQuoteRequests.length).to.be.greaterThan(0);
|
|
1725
|
+
expect(forwardAmounts.every((amount) => amount <= maxPerSourceOutput)).to
|
|
1726
|
+
.be.true;
|
|
1727
|
+
expect(
|
|
1728
|
+
reverseQuoteRequests.every(
|
|
1729
|
+
(params) => (params.toAmount as bigint) <= maxPerSourceOutput,
|
|
1730
|
+
),
|
|
1731
|
+
).to.be.true;
|
|
1721
1732
|
});
|
|
1722
1733
|
|
|
1723
1734
|
it('applies 5% buffer to total bridge amount', async () => {
|
|
@@ -1851,7 +1862,94 @@ describe('InventoryRebalancer E2E', () => {
|
|
|
1851
1862
|
expect(quotedTargetOutput).to.equal(1_050_000_000_000_000_000n);
|
|
1852
1863
|
});
|
|
1853
1864
|
|
|
1854
|
-
it('
|
|
1865
|
+
it('retries forward execution when reverse quote exceeds planned source capacity', async () => {
|
|
1866
|
+
const amount = BigInt(1e18);
|
|
1867
|
+
const sourceInventory = BigInt(2e18);
|
|
1868
|
+
const targetWithBuffer = (amount * 105n) / 100n;
|
|
1869
|
+
|
|
1870
|
+
const route = createTestRoute({ amount });
|
|
1871
|
+
createTestIntent({ amount });
|
|
1872
|
+
|
|
1873
|
+
inventoryRebalancer.setInventoryBalances({
|
|
1874
|
+
[SOLANA_CHAIN]: 0n,
|
|
1875
|
+
[ARBITRUM_CHAIN]: sourceInventory,
|
|
1876
|
+
});
|
|
1877
|
+
|
|
1878
|
+
bridge.quote
|
|
1879
|
+
.onFirstCall()
|
|
1880
|
+
.resolves(
|
|
1881
|
+
createMockBridgeQuote({
|
|
1882
|
+
fromAmount: sourceInventory,
|
|
1883
|
+
toAmount: sourceInventory,
|
|
1884
|
+
toAmountMin: sourceInventory,
|
|
1885
|
+
requestParams: {
|
|
1886
|
+
fromChain: 42161,
|
|
1887
|
+
toChain: 1399811149,
|
|
1888
|
+
fromToken: '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
|
|
1889
|
+
toToken: '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
|
|
1890
|
+
fromAddress: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
1891
|
+
toAddress: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
1892
|
+
fromAmount: sourceInventory,
|
|
1893
|
+
},
|
|
1894
|
+
}),
|
|
1895
|
+
)
|
|
1896
|
+
.onSecondCall()
|
|
1897
|
+
.resolves(
|
|
1898
|
+
createMockBridgeQuote({
|
|
1899
|
+
fromAmount: sourceInventory + 1n,
|
|
1900
|
+
toAmount: targetWithBuffer,
|
|
1901
|
+
toAmountMin: targetWithBuffer,
|
|
1902
|
+
requestParams: {
|
|
1903
|
+
fromChain: 42161,
|
|
1904
|
+
toChain: 1399811149,
|
|
1905
|
+
fromToken: '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
|
|
1906
|
+
toToken: '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
|
|
1907
|
+
fromAddress: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
1908
|
+
toAddress: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
1909
|
+
toAmount: targetWithBuffer,
|
|
1910
|
+
},
|
|
1911
|
+
}),
|
|
1912
|
+
)
|
|
1913
|
+
.onThirdCall()
|
|
1914
|
+
.resolves(
|
|
1915
|
+
createMockBridgeQuote({
|
|
1916
|
+
fromAmount: sourceInventory,
|
|
1917
|
+
toAmount: targetWithBuffer - 1n,
|
|
1918
|
+
toAmountMin: targetWithBuffer - 1n,
|
|
1919
|
+
requestParams: {
|
|
1920
|
+
fromChain: 42161,
|
|
1921
|
+
toChain: 1399811149,
|
|
1922
|
+
fromToken: '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
|
|
1923
|
+
toToken: '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
|
|
1924
|
+
fromAddress: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
1925
|
+
toAddress: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
1926
|
+
fromAmount: sourceInventory,
|
|
1927
|
+
},
|
|
1928
|
+
}),
|
|
1929
|
+
);
|
|
1930
|
+
|
|
1931
|
+
bridge.execute.resolves({
|
|
1932
|
+
txHash: '0xBridgeTxHash',
|
|
1933
|
+
fromChain: 42161,
|
|
1934
|
+
toChain: 1399811149,
|
|
1935
|
+
});
|
|
1936
|
+
|
|
1937
|
+
const results = await inventoryRebalancer.rebalance([route]);
|
|
1938
|
+
|
|
1939
|
+
expect(results).to.have.lengthOf(1);
|
|
1940
|
+
expect(results[0].success).to.be.true;
|
|
1941
|
+
expect(bridge.execute.calledOnce).to.be.true;
|
|
1942
|
+
expect(bridge.quote.callCount).to.equal(3);
|
|
1943
|
+
expect(bridge.quote.getCall(1).args[0].toAmount).to.equal(
|
|
1944
|
+
targetWithBuffer,
|
|
1945
|
+
);
|
|
1946
|
+
expect(bridge.quote.getCall(2).args[0].fromAmount).to.equal(
|
|
1947
|
+
sourceInventory,
|
|
1948
|
+
);
|
|
1949
|
+
expect(bridge.quote.getCall(2).args[0].toAmount).to.be.undefined;
|
|
1950
|
+
});
|
|
1951
|
+
|
|
1952
|
+
it('fails when forward retry still exceeds planned source capacity', async () => {
|
|
1855
1953
|
const amount = BigInt(1e18);
|
|
1856
1954
|
const sourceInventory = BigInt(2e18);
|
|
1857
1955
|
const targetWithBuffer = (amount * 105n) / 100n;
|
|
@@ -1898,6 +1996,23 @@ describe('InventoryRebalancer E2E', () => {
|
|
|
1898
1996
|
toAmount: targetWithBuffer,
|
|
1899
1997
|
},
|
|
1900
1998
|
}),
|
|
1999
|
+
)
|
|
2000
|
+
.onThirdCall()
|
|
2001
|
+
.resolves(
|
|
2002
|
+
createMockBridgeQuote({
|
|
2003
|
+
fromAmount: sourceInventory + 1n,
|
|
2004
|
+
toAmount: targetWithBuffer - 1n,
|
|
2005
|
+
toAmountMin: targetWithBuffer - 1n,
|
|
2006
|
+
requestParams: {
|
|
2007
|
+
fromChain: 42161,
|
|
2008
|
+
toChain: 1399811149,
|
|
2009
|
+
fromToken: '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
|
|
2010
|
+
toToken: '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
|
|
2011
|
+
fromAddress: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
2012
|
+
toAddress: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
2013
|
+
fromAmount: sourceInventory,
|
|
2014
|
+
},
|
|
2015
|
+
}),
|
|
1901
2016
|
);
|
|
1902
2017
|
|
|
1903
2018
|
const results = await inventoryRebalancer.rebalance([route]);
|
|
@@ -1906,7 +2021,213 @@ describe('InventoryRebalancer E2E', () => {
|
|
|
1906
2021
|
expect(results[0].success).to.be.false;
|
|
1907
2022
|
expect(results[0].error).to.include('All inventory movements failed');
|
|
1908
2023
|
expect(results[0].error).to.include('exceeded planned source capacity');
|
|
2024
|
+
expect(bridge.quote.callCount).to.equal(3);
|
|
1909
2025
|
expect(bridge.execute.called).to.be.false;
|
|
2026
|
+
expect(bridge.quote.getCall(1).args[0].toAmount).to.equal(
|
|
2027
|
+
targetWithBuffer,
|
|
2028
|
+
);
|
|
2029
|
+
expect(bridge.quote.getCall(2).args[0].fromAmount).to.equal(
|
|
2030
|
+
sourceInventory,
|
|
2031
|
+
);
|
|
2032
|
+
expect(bridge.quote.getCall(2).args[0].toAmount).to.be.undefined;
|
|
2033
|
+
});
|
|
2034
|
+
|
|
2035
|
+
it('fails gracefully when execute-time bridge quote rejects', async () => {
|
|
2036
|
+
const amount = BigInt(1e18);
|
|
2037
|
+
const sourceInventory = BigInt(2e18);
|
|
2038
|
+
|
|
2039
|
+
const route = createTestRoute({ amount });
|
|
2040
|
+
createTestIntent({ amount });
|
|
2041
|
+
|
|
2042
|
+
inventoryRebalancer.setInventoryBalances({
|
|
2043
|
+
[SOLANA_CHAIN]: 0n,
|
|
2044
|
+
[ARBITRUM_CHAIN]: sourceInventory,
|
|
2045
|
+
});
|
|
2046
|
+
|
|
2047
|
+
bridge.quote
|
|
2048
|
+
.onFirstCall()
|
|
2049
|
+
.resolves(
|
|
2050
|
+
createMockBridgeQuote({
|
|
2051
|
+
fromAmount: sourceInventory,
|
|
2052
|
+
toAmount: sourceInventory,
|
|
2053
|
+
toAmountMin: sourceInventory,
|
|
2054
|
+
requestParams: {
|
|
2055
|
+
fromChain: 42161,
|
|
2056
|
+
toChain: 1399811149,
|
|
2057
|
+
fromToken: '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
|
|
2058
|
+
toToken: '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
|
|
2059
|
+
fromAddress: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
2060
|
+
toAddress: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
2061
|
+
fromAmount: sourceInventory,
|
|
2062
|
+
},
|
|
2063
|
+
}),
|
|
2064
|
+
)
|
|
2065
|
+
.onSecondCall()
|
|
2066
|
+
.rejects(new Error('LiFi API timeout'));
|
|
2067
|
+
|
|
2068
|
+
const results = await inventoryRebalancer.rebalance([route]);
|
|
2069
|
+
|
|
2070
|
+
expect(results).to.have.lengthOf(1);
|
|
2071
|
+
expect(results[0].success).to.be.false;
|
|
2072
|
+
expect(results[0].error).to.include('All inventory movements failed');
|
|
2073
|
+
expect(results[0].error).to.include('LiFi API timeout');
|
|
2074
|
+
expect(bridge.quote.callCount).to.equal(2);
|
|
2075
|
+
expect(bridge.execute.called).to.be.false;
|
|
2076
|
+
});
|
|
2077
|
+
|
|
2078
|
+
it('preserves non-Error rejection reasons from parallel bridge execution', async () => {
|
|
2079
|
+
const amount = BigInt(1e18);
|
|
2080
|
+
const sourceInventory = BigInt(2e18);
|
|
2081
|
+
|
|
2082
|
+
const route = createTestRoute({ amount });
|
|
2083
|
+
createTestIntent({ amount });
|
|
2084
|
+
|
|
2085
|
+
inventoryRebalancer.setInventoryBalances({
|
|
2086
|
+
[SOLANA_CHAIN]: 0n,
|
|
2087
|
+
[ARBITRUM_CHAIN]: sourceInventory,
|
|
2088
|
+
});
|
|
2089
|
+
|
|
2090
|
+
bridge.quote.onFirstCall().resolves(
|
|
2091
|
+
createMockBridgeQuote({
|
|
2092
|
+
fromAmount: sourceInventory,
|
|
2093
|
+
toAmount: sourceInventory,
|
|
2094
|
+
toAmountMin: sourceInventory,
|
|
2095
|
+
requestParams: {
|
|
2096
|
+
fromChain: 42161,
|
|
2097
|
+
toChain: 1399811149,
|
|
2098
|
+
fromToken: '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
|
|
2099
|
+
toToken: '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
|
|
2100
|
+
fromAddress: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
2101
|
+
toAddress: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
2102
|
+
fromAmount: sourceInventory,
|
|
2103
|
+
},
|
|
2104
|
+
}),
|
|
2105
|
+
);
|
|
2106
|
+
|
|
2107
|
+
// Simulate a foreign promise rejecting with a non-Error reason at runtime.
|
|
2108
|
+
const rejectionReason = 'bridge exploded' as unknown as Error;
|
|
2109
|
+
|
|
2110
|
+
const executeInventoryMovementStub = Sinon.stub(
|
|
2111
|
+
inventoryRebalancer as unknown as {
|
|
2112
|
+
executeInventoryMovement: () => Promise<unknown>;
|
|
2113
|
+
},
|
|
2114
|
+
'executeInventoryMovement',
|
|
2115
|
+
).callsFake(() =>
|
|
2116
|
+
Promise.resolve().then(() => {
|
|
2117
|
+
throw rejectionReason;
|
|
2118
|
+
}),
|
|
2119
|
+
);
|
|
2120
|
+
|
|
2121
|
+
const results = await inventoryRebalancer.rebalance([route]);
|
|
2122
|
+
|
|
2123
|
+
expect(results).to.have.lengthOf(1);
|
|
2124
|
+
expect(results[0].success).to.be.false;
|
|
2125
|
+
expect(results[0].error).to.include('All inventory movements failed');
|
|
2126
|
+
expect(results[0].error).to.include('arbitrum: bridge exploded');
|
|
2127
|
+
expect(executeInventoryMovementStub.calledOnce).to.be.true;
|
|
2128
|
+
expect(bridge.execute.called).to.be.false;
|
|
2129
|
+
|
|
2130
|
+
executeInventoryMovementStub.restore();
|
|
2131
|
+
});
|
|
2132
|
+
|
|
2133
|
+
it('logs actual successful bridge output floors instead of planned output totals', async () => {
|
|
2134
|
+
const amount = BigInt(1e18);
|
|
2135
|
+
const perChainInventory = BigInt(0.6e18);
|
|
2136
|
+
const logger = {
|
|
2137
|
+
info: Sinon.stub(),
|
|
2138
|
+
debug: Sinon.stub(),
|
|
2139
|
+
warn: Sinon.stub(),
|
|
2140
|
+
error: Sinon.stub(),
|
|
2141
|
+
};
|
|
2142
|
+
|
|
2143
|
+
const localRebalancer = new InventoryRebalancer(
|
|
2144
|
+
config,
|
|
2145
|
+
actionTracker as unknown as IActionTracker,
|
|
2146
|
+
{ lifi: bridge as unknown as IExternalBridge },
|
|
2147
|
+
warpCore as unknown as WarpCore,
|
|
2148
|
+
multiProvider as unknown as MultiProvider,
|
|
2149
|
+
logger as unknown as Logger,
|
|
2150
|
+
);
|
|
2151
|
+
|
|
2152
|
+
const route = createTestRoute({ amount });
|
|
2153
|
+
createTestIntent({ amount });
|
|
2154
|
+
|
|
2155
|
+
localRebalancer.setInventoryBalances({
|
|
2156
|
+
[SOLANA_CHAIN]: 0n,
|
|
2157
|
+
[ARBITRUM_CHAIN]: perChainInventory,
|
|
2158
|
+
[BASE_CHAIN]: perChainInventory,
|
|
2159
|
+
});
|
|
2160
|
+
|
|
2161
|
+
bridge.quote
|
|
2162
|
+
.onCall(0)
|
|
2163
|
+
.resolves(
|
|
2164
|
+
createMockBridgeQuote({
|
|
2165
|
+
fromAmount: perChainInventory,
|
|
2166
|
+
toAmount: BigInt(0.525e18),
|
|
2167
|
+
toAmountMin: BigInt(0.525e18),
|
|
2168
|
+
}),
|
|
2169
|
+
)
|
|
2170
|
+
.onCall(1)
|
|
2171
|
+
.resolves(
|
|
2172
|
+
createMockBridgeQuote({
|
|
2173
|
+
fromAmount: perChainInventory,
|
|
2174
|
+
toAmount: BigInt(0.525e18),
|
|
2175
|
+
toAmountMin: BigInt(0.525e18),
|
|
2176
|
+
}),
|
|
2177
|
+
)
|
|
2178
|
+
.onCall(2)
|
|
2179
|
+
.resolves(
|
|
2180
|
+
createMockBridgeQuote({
|
|
2181
|
+
fromAmount: perChainInventory,
|
|
2182
|
+
toAmount: BigInt(0.505e18),
|
|
2183
|
+
toAmountMin: BigInt(0.5e18),
|
|
2184
|
+
}),
|
|
2185
|
+
)
|
|
2186
|
+
.onCall(3)
|
|
2187
|
+
.resolves(
|
|
2188
|
+
createMockBridgeQuote({
|
|
2189
|
+
fromAmount: perChainInventory,
|
|
2190
|
+
toAmount: BigInt(0.515e18),
|
|
2191
|
+
toAmountMin: BigInt(0.51e18),
|
|
2192
|
+
}),
|
|
2193
|
+
);
|
|
2194
|
+
|
|
2195
|
+
bridge.execute
|
|
2196
|
+
.onFirstCall()
|
|
2197
|
+
.resolves({
|
|
2198
|
+
txHash: '0xSuccessTxHash1',
|
|
2199
|
+
fromChain: 42161,
|
|
2200
|
+
toChain: 1399811149,
|
|
2201
|
+
})
|
|
2202
|
+
.onSecondCall()
|
|
2203
|
+
.resolves({
|
|
2204
|
+
txHash: '0xSuccessTxHash2',
|
|
2205
|
+
fromChain: 8453,
|
|
2206
|
+
toChain: 1399811149,
|
|
2207
|
+
});
|
|
2208
|
+
|
|
2209
|
+
const results = await localRebalancer.rebalance([route]);
|
|
2210
|
+
|
|
2211
|
+
expect(results).to.have.lengthOf(1);
|
|
2212
|
+
expect(results[0].success).to.be.true;
|
|
2213
|
+
expect(bridge.execute.callCount).to.equal(2);
|
|
2214
|
+
|
|
2215
|
+
const summaryCall = logger.info
|
|
2216
|
+
.getCalls()
|
|
2217
|
+
.find(
|
|
2218
|
+
(call: ReturnType<typeof logger.info.getCall>) =>
|
|
2219
|
+
call.args[0] &&
|
|
2220
|
+
typeof call.args[0] === 'object' &&
|
|
2221
|
+
Object.prototype.hasOwnProperty.call(
|
|
2222
|
+
call.args[0],
|
|
2223
|
+
'totalQuotedOutputMin',
|
|
2224
|
+
),
|
|
2225
|
+
);
|
|
2226
|
+
|
|
2227
|
+
assert(summaryCall, 'Expected summary log with totalQuotedOutputMin');
|
|
2228
|
+
expect(summaryCall.args[0].totalQuotedOutputMin).to.equal(
|
|
2229
|
+
BigInt(1.01e18).toString(),
|
|
2230
|
+
);
|
|
1910
2231
|
});
|
|
1911
2232
|
|
|
1912
2233
|
it('continues when some bridges fail', async () => {
|
|
@@ -2177,7 +2498,9 @@ describe('InventoryRebalancer E2E', () => {
|
|
|
2177
2498
|
expect(results).to.have.lengthOf(1);
|
|
2178
2499
|
expect(results[0].success).to.be.true;
|
|
2179
2500
|
expect(bridge.execute.calledOnce).to.be.true;
|
|
2180
|
-
expect(bridge.quote.
|
|
2501
|
+
expect(bridge.quote.callCount).to.equal(2);
|
|
2502
|
+
expect(bridge.quote.getCall(1).args[0].fromAmount).to.equal(tokenBalance);
|
|
2503
|
+
expect(bridge.quote.getCall(1).args[0].toAmount).to.be.undefined;
|
|
2181
2504
|
});
|
|
2182
2505
|
|
|
2183
2506
|
it('calculates max viable amount as inventory minus (gasCosts * 20) for native tokens', async () => {
|
|
@@ -2267,9 +2590,10 @@ describe('InventoryRebalancer E2E', () => {
|
|
|
2267
2590
|
expect(results).to.have.lengthOf(1);
|
|
2268
2591
|
expect(results[0].success).to.be.true;
|
|
2269
2592
|
expect(bridge.execute.calledOnce).to.be.true;
|
|
2593
|
+
expect(bridge.quote.callCount).to.equal(3);
|
|
2270
2594
|
|
|
2271
|
-
expect(bridge.quote.getCall(1)
|
|
2272
|
-
const executionTargetOutput = bridge.quote.getCall(2)
|
|
2595
|
+
expect(bridge.quote.getCall(1).args[0].fromAmount).to.equal(maxViable);
|
|
2596
|
+
const executionTargetOutput = bridge.quote.getCall(2).args[0].toAmount;
|
|
2273
2597
|
expect(executionTargetOutput).to.be.a('bigint');
|
|
2274
2598
|
if (executionTargetOutput === undefined) {
|
|
2275
2599
|
throw new Error('Expected reverse quote to set toAmount');
|
|
@@ -2278,6 +2602,111 @@ describe('InventoryRebalancer E2E', () => {
|
|
|
2278
2602
|
expect(executionTargetOutput <= maxViable).to.be.true;
|
|
2279
2603
|
});
|
|
2280
2604
|
|
|
2605
|
+
it('uses forward quote when target output exactly matches source capacity', async () => {
|
|
2606
|
+
const amount = 1000n;
|
|
2607
|
+
const rawBalance = 1100n;
|
|
2608
|
+
const targetWithBuffer = 1050n;
|
|
2609
|
+
|
|
2610
|
+
const route = createTestRoute({ amount });
|
|
2611
|
+
createTestIntent({ amount });
|
|
2612
|
+
|
|
2613
|
+
inventoryRebalancer.setInventoryBalances({
|
|
2614
|
+
[SOLANA_CHAIN]: 0n,
|
|
2615
|
+
[ARBITRUM_CHAIN]: rawBalance,
|
|
2616
|
+
});
|
|
2617
|
+
|
|
2618
|
+
bridge.quote
|
|
2619
|
+
.onFirstCall()
|
|
2620
|
+
.resolves(
|
|
2621
|
+
createMockBridgeQuote({
|
|
2622
|
+
fromAmount: rawBalance,
|
|
2623
|
+
toAmount: targetWithBuffer,
|
|
2624
|
+
toAmountMin: targetWithBuffer,
|
|
2625
|
+
}),
|
|
2626
|
+
)
|
|
2627
|
+
.onSecondCall()
|
|
2628
|
+
.resolves(
|
|
2629
|
+
createMockBridgeQuote({
|
|
2630
|
+
fromAmount: rawBalance,
|
|
2631
|
+
toAmount: targetWithBuffer,
|
|
2632
|
+
toAmountMin: targetWithBuffer,
|
|
2633
|
+
}),
|
|
2634
|
+
);
|
|
2635
|
+
|
|
2636
|
+
bridge.execute.resolves({
|
|
2637
|
+
txHash: '0xForwardBoundaryBridgeTxHash',
|
|
2638
|
+
fromChain: 42161,
|
|
2639
|
+
toChain: 1399811149,
|
|
2640
|
+
});
|
|
2641
|
+
|
|
2642
|
+
const results = await inventoryRebalancer.rebalance([route]);
|
|
2643
|
+
|
|
2644
|
+
expect(results).to.have.lengthOf(1);
|
|
2645
|
+
expect(results[0].success).to.be.true;
|
|
2646
|
+
expect(bridge.execute.calledOnce).to.be.true;
|
|
2647
|
+
expect(bridge.quote.callCount).to.equal(2);
|
|
2648
|
+
expect(bridge.quote.getCall(1).args[0].fromAmount).to.equal(rawBalance);
|
|
2649
|
+
expect(bridge.quote.getCall(1).args[0].toAmount).to.be.undefined;
|
|
2650
|
+
});
|
|
2651
|
+
|
|
2652
|
+
it('retries reverse quote as forward when exact-output exceeds source capacity', async () => {
|
|
2653
|
+
const amount = 1000n;
|
|
2654
|
+
const rawBalance = 1100n;
|
|
2655
|
+
const targetWithBuffer = 1050n;
|
|
2656
|
+
|
|
2657
|
+
const route = createTestRoute({ amount });
|
|
2658
|
+
createTestIntent({ amount });
|
|
2659
|
+
|
|
2660
|
+
inventoryRebalancer.setInventoryBalances({
|
|
2661
|
+
[SOLANA_CHAIN]: 0n,
|
|
2662
|
+
[ARBITRUM_CHAIN]: rawBalance,
|
|
2663
|
+
});
|
|
2664
|
+
|
|
2665
|
+
bridge.quote
|
|
2666
|
+
.onFirstCall()
|
|
2667
|
+
.resolves(
|
|
2668
|
+
createMockBridgeQuote({
|
|
2669
|
+
fromAmount: rawBalance,
|
|
2670
|
+
toAmount: 1051n,
|
|
2671
|
+
toAmountMin: 1051n,
|
|
2672
|
+
}),
|
|
2673
|
+
)
|
|
2674
|
+
.onSecondCall()
|
|
2675
|
+
.resolves(
|
|
2676
|
+
createMockBridgeQuote({
|
|
2677
|
+
fromAmount: rawBalance + 1n,
|
|
2678
|
+
toAmount: targetWithBuffer,
|
|
2679
|
+
toAmountMin: targetWithBuffer,
|
|
2680
|
+
}),
|
|
2681
|
+
)
|
|
2682
|
+
.onThirdCall()
|
|
2683
|
+
.resolves(
|
|
2684
|
+
createMockBridgeQuote({
|
|
2685
|
+
fromAmount: rawBalance,
|
|
2686
|
+
toAmount: 1049n,
|
|
2687
|
+
toAmountMin: 1049n,
|
|
2688
|
+
}),
|
|
2689
|
+
);
|
|
2690
|
+
|
|
2691
|
+
bridge.execute.resolves({
|
|
2692
|
+
txHash: '0xForwardFallbackBridgeTxHash',
|
|
2693
|
+
fromChain: 42161,
|
|
2694
|
+
toChain: 1399811149,
|
|
2695
|
+
});
|
|
2696
|
+
|
|
2697
|
+
const results = await inventoryRebalancer.rebalance([route]);
|
|
2698
|
+
|
|
2699
|
+
expect(results).to.have.lengthOf(1);
|
|
2700
|
+
expect(results[0].success).to.be.true;
|
|
2701
|
+
expect(bridge.execute.calledOnce).to.be.true;
|
|
2702
|
+
expect(bridge.quote.callCount).to.equal(3);
|
|
2703
|
+
expect(bridge.quote.getCall(1).args[0].toAmount).to.equal(
|
|
2704
|
+
targetWithBuffer,
|
|
2705
|
+
);
|
|
2706
|
+
expect(bridge.quote.getCall(2).args[0].fromAmount).to.equal(rawBalance);
|
|
2707
|
+
expect(bridge.quote.getCall(2).args[0].toAmount).to.be.undefined;
|
|
2708
|
+
});
|
|
2709
|
+
|
|
2281
2710
|
it('handles quote failures gracefully by skipping the source chain', async () => {
|
|
2282
2711
|
// Setup: Native token where quote fails
|
|
2283
2712
|
const arbitrumToken = {
|