@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
|
@@ -4,7 +4,7 @@ import { Wallet } from 'ethers';
|
|
|
4
4
|
import { pino } from 'pino';
|
|
5
5
|
import Sinon from 'sinon';
|
|
6
6
|
import { ProviderType, TokenStandard, WarpTxCategory, } from '@hyperlane-xyz/sdk';
|
|
7
|
-
import { ProtocolType } from '@hyperlane-xyz/utils';
|
|
7
|
+
import { assert, ProtocolType } from '@hyperlane-xyz/utils';
|
|
8
8
|
import { ExternalBridgeType } from '../config/types.js';
|
|
9
9
|
import { createMockBridgeQuote } from '../test/lifiMocks.js';
|
|
10
10
|
import { InventoryRebalancer, } from './InventoryRebalancer.js';
|
|
@@ -1373,22 +1373,20 @@ describe('InventoryRebalancer E2E', () => {
|
|
|
1373
1373
|
expect(results).to.have.lengthOf(1);
|
|
1374
1374
|
expect(results[0].success).to.be.true;
|
|
1375
1375
|
expect(bridge.execute.callCount).to.equal(2);
|
|
1376
|
-
const reverseQuoteRequests = bridge.quote
|
|
1377
|
-
.getCalls()
|
|
1378
|
-
.map((call) => call.args[0])
|
|
1379
|
-
.filter((params) => params.toAmount !== undefined);
|
|
1380
|
-
expect(reverseQuoteRequests).to.have.lengthOf(2);
|
|
1381
|
-
const requestedOutputs = reverseQuoteRequests
|
|
1382
|
-
.map((params) => params.toAmount)
|
|
1383
|
-
.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
1384
1376
|
const maxPerSourceOutput = perChainInventory - reservedGas;
|
|
1385
|
-
const
|
|
1386
|
-
|
|
1387
|
-
.
|
|
1388
|
-
|
|
1389
|
-
expect(
|
|
1390
|
-
|
|
1391
|
-
|
|
1377
|
+
const executionQuoteRequests = bridge.quote
|
|
1378
|
+
.getCalls()
|
|
1379
|
+
.slice(-2)
|
|
1380
|
+
.map((call) => call.args[0]);
|
|
1381
|
+
expect(executionQuoteRequests).to.have.lengthOf(2);
|
|
1382
|
+
const forwardQuoteRequests = executionQuoteRequests.filter((params) => params.fromAmount !== undefined);
|
|
1383
|
+
const reverseQuoteRequests = executionQuoteRequests.filter((params) => params.toAmount !== undefined);
|
|
1384
|
+
const forwardAmounts = forwardQuoteRequests.map((params) => params.fromAmount);
|
|
1385
|
+
expect(forwardQuoteRequests.length + reverseQuoteRequests.length).to.equal(2);
|
|
1386
|
+
expect(forwardQuoteRequests.length).to.be.greaterThan(0);
|
|
1387
|
+
expect(forwardAmounts.every((amount) => amount <= maxPerSourceOutput)).to
|
|
1388
|
+
.be.true;
|
|
1389
|
+
expect(reverseQuoteRequests.every((params) => params.toAmount <= maxPerSourceOutput)).to.be.true;
|
|
1392
1390
|
});
|
|
1393
1391
|
it('applies 5% buffer to total bridge amount', async () => {
|
|
1394
1392
|
// Need 1 ETH -> should plan to bridge 1.05 ETH total (with 5% buffer)
|
|
@@ -1500,7 +1498,77 @@ describe('InventoryRebalancer E2E', () => {
|
|
|
1500
1498
|
await inventoryRebalancer.rebalance([route]);
|
|
1501
1499
|
expect(quotedTargetOutput).to.equal(1050000000000000000n);
|
|
1502
1500
|
});
|
|
1503
|
-
it('
|
|
1501
|
+
it('retries forward execution when reverse quote exceeds planned source capacity', async () => {
|
|
1502
|
+
const amount = BigInt(1e18);
|
|
1503
|
+
const sourceInventory = BigInt(2e18);
|
|
1504
|
+
const targetWithBuffer = (amount * 105n) / 100n;
|
|
1505
|
+
const route = createTestRoute({ amount });
|
|
1506
|
+
createTestIntent({ amount });
|
|
1507
|
+
inventoryRebalancer.setInventoryBalances({
|
|
1508
|
+
[SOLANA_CHAIN]: 0n,
|
|
1509
|
+
[ARBITRUM_CHAIN]: sourceInventory,
|
|
1510
|
+
});
|
|
1511
|
+
bridge.quote
|
|
1512
|
+
.onFirstCall()
|
|
1513
|
+
.resolves(createMockBridgeQuote({
|
|
1514
|
+
fromAmount: sourceInventory,
|
|
1515
|
+
toAmount: sourceInventory,
|
|
1516
|
+
toAmountMin: sourceInventory,
|
|
1517
|
+
requestParams: {
|
|
1518
|
+
fromChain: 42161,
|
|
1519
|
+
toChain: 1399811149,
|
|
1520
|
+
fromToken: '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
|
|
1521
|
+
toToken: '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
|
|
1522
|
+
fromAddress: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
1523
|
+
toAddress: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
1524
|
+
fromAmount: sourceInventory,
|
|
1525
|
+
},
|
|
1526
|
+
}))
|
|
1527
|
+
.onSecondCall()
|
|
1528
|
+
.resolves(createMockBridgeQuote({
|
|
1529
|
+
fromAmount: sourceInventory + 1n,
|
|
1530
|
+
toAmount: targetWithBuffer,
|
|
1531
|
+
toAmountMin: targetWithBuffer,
|
|
1532
|
+
requestParams: {
|
|
1533
|
+
fromChain: 42161,
|
|
1534
|
+
toChain: 1399811149,
|
|
1535
|
+
fromToken: '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
|
|
1536
|
+
toToken: '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
|
|
1537
|
+
fromAddress: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
1538
|
+
toAddress: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
1539
|
+
toAmount: targetWithBuffer,
|
|
1540
|
+
},
|
|
1541
|
+
}))
|
|
1542
|
+
.onThirdCall()
|
|
1543
|
+
.resolves(createMockBridgeQuote({
|
|
1544
|
+
fromAmount: sourceInventory,
|
|
1545
|
+
toAmount: targetWithBuffer - 1n,
|
|
1546
|
+
toAmountMin: targetWithBuffer - 1n,
|
|
1547
|
+
requestParams: {
|
|
1548
|
+
fromChain: 42161,
|
|
1549
|
+
toChain: 1399811149,
|
|
1550
|
+
fromToken: '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
|
|
1551
|
+
toToken: '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
|
|
1552
|
+
fromAddress: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
1553
|
+
toAddress: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
1554
|
+
fromAmount: sourceInventory,
|
|
1555
|
+
},
|
|
1556
|
+
}));
|
|
1557
|
+
bridge.execute.resolves({
|
|
1558
|
+
txHash: '0xBridgeTxHash',
|
|
1559
|
+
fromChain: 42161,
|
|
1560
|
+
toChain: 1399811149,
|
|
1561
|
+
});
|
|
1562
|
+
const results = await inventoryRebalancer.rebalance([route]);
|
|
1563
|
+
expect(results).to.have.lengthOf(1);
|
|
1564
|
+
expect(results[0].success).to.be.true;
|
|
1565
|
+
expect(bridge.execute.calledOnce).to.be.true;
|
|
1566
|
+
expect(bridge.quote.callCount).to.equal(3);
|
|
1567
|
+
expect(bridge.quote.getCall(1).args[0].toAmount).to.equal(targetWithBuffer);
|
|
1568
|
+
expect(bridge.quote.getCall(2).args[0].fromAmount).to.equal(sourceInventory);
|
|
1569
|
+
expect(bridge.quote.getCall(2).args[0].toAmount).to.be.undefined;
|
|
1570
|
+
});
|
|
1571
|
+
it('fails when forward retry still exceeds planned source capacity', async () => {
|
|
1504
1572
|
const amount = BigInt(1e18);
|
|
1505
1573
|
const sourceInventory = BigInt(2e18);
|
|
1506
1574
|
const targetWithBuffer = (amount * 105n) / 100n;
|
|
@@ -1540,14 +1608,172 @@ describe('InventoryRebalancer E2E', () => {
|
|
|
1540
1608
|
toAddress: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
1541
1609
|
toAmount: targetWithBuffer,
|
|
1542
1610
|
},
|
|
1611
|
+
}))
|
|
1612
|
+
.onThirdCall()
|
|
1613
|
+
.resolves(createMockBridgeQuote({
|
|
1614
|
+
fromAmount: sourceInventory + 1n,
|
|
1615
|
+
toAmount: targetWithBuffer - 1n,
|
|
1616
|
+
toAmountMin: targetWithBuffer - 1n,
|
|
1617
|
+
requestParams: {
|
|
1618
|
+
fromChain: 42161,
|
|
1619
|
+
toChain: 1399811149,
|
|
1620
|
+
fromToken: '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
|
|
1621
|
+
toToken: '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
|
|
1622
|
+
fromAddress: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
1623
|
+
toAddress: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
1624
|
+
fromAmount: sourceInventory,
|
|
1625
|
+
},
|
|
1543
1626
|
}));
|
|
1544
1627
|
const results = await inventoryRebalancer.rebalance([route]);
|
|
1545
1628
|
expect(results).to.have.lengthOf(1);
|
|
1546
1629
|
expect(results[0].success).to.be.false;
|
|
1547
1630
|
expect(results[0].error).to.include('All inventory movements failed');
|
|
1548
1631
|
expect(results[0].error).to.include('exceeded planned source capacity');
|
|
1632
|
+
expect(bridge.quote.callCount).to.equal(3);
|
|
1633
|
+
expect(bridge.execute.called).to.be.false;
|
|
1634
|
+
expect(bridge.quote.getCall(1).args[0].toAmount).to.equal(targetWithBuffer);
|
|
1635
|
+
expect(bridge.quote.getCall(2).args[0].fromAmount).to.equal(sourceInventory);
|
|
1636
|
+
expect(bridge.quote.getCall(2).args[0].toAmount).to.be.undefined;
|
|
1637
|
+
});
|
|
1638
|
+
it('fails gracefully when execute-time bridge quote rejects', async () => {
|
|
1639
|
+
const amount = BigInt(1e18);
|
|
1640
|
+
const sourceInventory = BigInt(2e18);
|
|
1641
|
+
const route = createTestRoute({ amount });
|
|
1642
|
+
createTestIntent({ amount });
|
|
1643
|
+
inventoryRebalancer.setInventoryBalances({
|
|
1644
|
+
[SOLANA_CHAIN]: 0n,
|
|
1645
|
+
[ARBITRUM_CHAIN]: sourceInventory,
|
|
1646
|
+
});
|
|
1647
|
+
bridge.quote
|
|
1648
|
+
.onFirstCall()
|
|
1649
|
+
.resolves(createMockBridgeQuote({
|
|
1650
|
+
fromAmount: sourceInventory,
|
|
1651
|
+
toAmount: sourceInventory,
|
|
1652
|
+
toAmountMin: sourceInventory,
|
|
1653
|
+
requestParams: {
|
|
1654
|
+
fromChain: 42161,
|
|
1655
|
+
toChain: 1399811149,
|
|
1656
|
+
fromToken: '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
|
|
1657
|
+
toToken: '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
|
|
1658
|
+
fromAddress: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
1659
|
+
toAddress: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
1660
|
+
fromAmount: sourceInventory,
|
|
1661
|
+
},
|
|
1662
|
+
}))
|
|
1663
|
+
.onSecondCall()
|
|
1664
|
+
.rejects(new Error('LiFi API timeout'));
|
|
1665
|
+
const results = await inventoryRebalancer.rebalance([route]);
|
|
1666
|
+
expect(results).to.have.lengthOf(1);
|
|
1667
|
+
expect(results[0].success).to.be.false;
|
|
1668
|
+
expect(results[0].error).to.include('All inventory movements failed');
|
|
1669
|
+
expect(results[0].error).to.include('LiFi API timeout');
|
|
1670
|
+
expect(bridge.quote.callCount).to.equal(2);
|
|
1549
1671
|
expect(bridge.execute.called).to.be.false;
|
|
1550
1672
|
});
|
|
1673
|
+
it('preserves non-Error rejection reasons from parallel bridge execution', async () => {
|
|
1674
|
+
const amount = BigInt(1e18);
|
|
1675
|
+
const sourceInventory = BigInt(2e18);
|
|
1676
|
+
const route = createTestRoute({ amount });
|
|
1677
|
+
createTestIntent({ amount });
|
|
1678
|
+
inventoryRebalancer.setInventoryBalances({
|
|
1679
|
+
[SOLANA_CHAIN]: 0n,
|
|
1680
|
+
[ARBITRUM_CHAIN]: sourceInventory,
|
|
1681
|
+
});
|
|
1682
|
+
bridge.quote.onFirstCall().resolves(createMockBridgeQuote({
|
|
1683
|
+
fromAmount: sourceInventory,
|
|
1684
|
+
toAmount: sourceInventory,
|
|
1685
|
+
toAmountMin: sourceInventory,
|
|
1686
|
+
requestParams: {
|
|
1687
|
+
fromChain: 42161,
|
|
1688
|
+
toChain: 1399811149,
|
|
1689
|
+
fromToken: '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
|
|
1690
|
+
toToken: '0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC',
|
|
1691
|
+
fromAddress: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
1692
|
+
toAddress: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
1693
|
+
fromAmount: sourceInventory,
|
|
1694
|
+
},
|
|
1695
|
+
}));
|
|
1696
|
+
// Simulate a foreign promise rejecting with a non-Error reason at runtime.
|
|
1697
|
+
const rejectionReason = 'bridge exploded';
|
|
1698
|
+
const executeInventoryMovementStub = Sinon.stub(inventoryRebalancer, 'executeInventoryMovement').callsFake(() => Promise.resolve().then(() => {
|
|
1699
|
+
throw rejectionReason;
|
|
1700
|
+
}));
|
|
1701
|
+
const results = await inventoryRebalancer.rebalance([route]);
|
|
1702
|
+
expect(results).to.have.lengthOf(1);
|
|
1703
|
+
expect(results[0].success).to.be.false;
|
|
1704
|
+
expect(results[0].error).to.include('All inventory movements failed');
|
|
1705
|
+
expect(results[0].error).to.include('arbitrum: bridge exploded');
|
|
1706
|
+
expect(executeInventoryMovementStub.calledOnce).to.be.true;
|
|
1707
|
+
expect(bridge.execute.called).to.be.false;
|
|
1708
|
+
executeInventoryMovementStub.restore();
|
|
1709
|
+
});
|
|
1710
|
+
it('logs actual successful bridge output floors instead of planned output totals', async () => {
|
|
1711
|
+
const amount = BigInt(1e18);
|
|
1712
|
+
const perChainInventory = BigInt(0.6e18);
|
|
1713
|
+
const logger = {
|
|
1714
|
+
info: Sinon.stub(),
|
|
1715
|
+
debug: Sinon.stub(),
|
|
1716
|
+
warn: Sinon.stub(),
|
|
1717
|
+
error: Sinon.stub(),
|
|
1718
|
+
};
|
|
1719
|
+
const localRebalancer = new InventoryRebalancer(config, actionTracker, { lifi: bridge }, warpCore, multiProvider, logger);
|
|
1720
|
+
const route = createTestRoute({ amount });
|
|
1721
|
+
createTestIntent({ amount });
|
|
1722
|
+
localRebalancer.setInventoryBalances({
|
|
1723
|
+
[SOLANA_CHAIN]: 0n,
|
|
1724
|
+
[ARBITRUM_CHAIN]: perChainInventory,
|
|
1725
|
+
[BASE_CHAIN]: perChainInventory,
|
|
1726
|
+
});
|
|
1727
|
+
bridge.quote
|
|
1728
|
+
.onCall(0)
|
|
1729
|
+
.resolves(createMockBridgeQuote({
|
|
1730
|
+
fromAmount: perChainInventory,
|
|
1731
|
+
toAmount: BigInt(0.525e18),
|
|
1732
|
+
toAmountMin: BigInt(0.525e18),
|
|
1733
|
+
}))
|
|
1734
|
+
.onCall(1)
|
|
1735
|
+
.resolves(createMockBridgeQuote({
|
|
1736
|
+
fromAmount: perChainInventory,
|
|
1737
|
+
toAmount: BigInt(0.525e18),
|
|
1738
|
+
toAmountMin: BigInt(0.525e18),
|
|
1739
|
+
}))
|
|
1740
|
+
.onCall(2)
|
|
1741
|
+
.resolves(createMockBridgeQuote({
|
|
1742
|
+
fromAmount: perChainInventory,
|
|
1743
|
+
toAmount: BigInt(0.505e18),
|
|
1744
|
+
toAmountMin: BigInt(0.5e18),
|
|
1745
|
+
}))
|
|
1746
|
+
.onCall(3)
|
|
1747
|
+
.resolves(createMockBridgeQuote({
|
|
1748
|
+
fromAmount: perChainInventory,
|
|
1749
|
+
toAmount: BigInt(0.515e18),
|
|
1750
|
+
toAmountMin: BigInt(0.51e18),
|
|
1751
|
+
}));
|
|
1752
|
+
bridge.execute
|
|
1753
|
+
.onFirstCall()
|
|
1754
|
+
.resolves({
|
|
1755
|
+
txHash: '0xSuccessTxHash1',
|
|
1756
|
+
fromChain: 42161,
|
|
1757
|
+
toChain: 1399811149,
|
|
1758
|
+
})
|
|
1759
|
+
.onSecondCall()
|
|
1760
|
+
.resolves({
|
|
1761
|
+
txHash: '0xSuccessTxHash2',
|
|
1762
|
+
fromChain: 8453,
|
|
1763
|
+
toChain: 1399811149,
|
|
1764
|
+
});
|
|
1765
|
+
const results = await localRebalancer.rebalance([route]);
|
|
1766
|
+
expect(results).to.have.lengthOf(1);
|
|
1767
|
+
expect(results[0].success).to.be.true;
|
|
1768
|
+
expect(bridge.execute.callCount).to.equal(2);
|
|
1769
|
+
const summaryCall = logger.info
|
|
1770
|
+
.getCalls()
|
|
1771
|
+
.find((call) => call.args[0] &&
|
|
1772
|
+
typeof call.args[0] === 'object' &&
|
|
1773
|
+
Object.prototype.hasOwnProperty.call(call.args[0], 'totalQuotedOutputMin'));
|
|
1774
|
+
assert(summaryCall, 'Expected summary log with totalQuotedOutputMin');
|
|
1775
|
+
expect(summaryCall.args[0].totalQuotedOutputMin).to.equal(BigInt(1.01e18).toString());
|
|
1776
|
+
});
|
|
1551
1777
|
it('continues when some bridges fail', async () => {
|
|
1552
1778
|
const amount = BigInt(1e18);
|
|
1553
1779
|
const perChainInventory = BigInt(0.6e18);
|
|
@@ -1763,7 +1989,9 @@ describe('InventoryRebalancer E2E', () => {
|
|
|
1763
1989
|
expect(results).to.have.lengthOf(1);
|
|
1764
1990
|
expect(results[0].success).to.be.true;
|
|
1765
1991
|
expect(bridge.execute.calledOnce).to.be.true;
|
|
1766
|
-
expect(bridge.quote.
|
|
1992
|
+
expect(bridge.quote.callCount).to.equal(2);
|
|
1993
|
+
expect(bridge.quote.getCall(1).args[0].fromAmount).to.equal(tokenBalance);
|
|
1994
|
+
expect(bridge.quote.getCall(1).args[0].toAmount).to.be.undefined;
|
|
1767
1995
|
});
|
|
1768
1996
|
it('calculates max viable amount as inventory minus (gasCosts * 20) for native tokens', async () => {
|
|
1769
1997
|
// Setup: Native token with enough balance for viable bridge
|
|
@@ -1838,8 +2066,9 @@ describe('InventoryRebalancer E2E', () => {
|
|
|
1838
2066
|
expect(results).to.have.lengthOf(1);
|
|
1839
2067
|
expect(results[0].success).to.be.true;
|
|
1840
2068
|
expect(bridge.execute.calledOnce).to.be.true;
|
|
1841
|
-
expect(bridge.quote.
|
|
1842
|
-
|
|
2069
|
+
expect(bridge.quote.callCount).to.equal(3);
|
|
2070
|
+
expect(bridge.quote.getCall(1).args[0].fromAmount).to.equal(maxViable);
|
|
2071
|
+
const executionTargetOutput = bridge.quote.getCall(2).args[0].toAmount;
|
|
1843
2072
|
expect(executionTargetOutput).to.be.a('bigint');
|
|
1844
2073
|
if (executionTargetOutput === undefined) {
|
|
1845
2074
|
throw new Error('Expected reverse quote to set toAmount');
|
|
@@ -1847,6 +2076,85 @@ describe('InventoryRebalancer E2E', () => {
|
|
|
1847
2076
|
expect(executionTargetOutput > amount).to.be.true;
|
|
1848
2077
|
expect(executionTargetOutput <= maxViable).to.be.true;
|
|
1849
2078
|
});
|
|
2079
|
+
it('uses forward quote when target output exactly matches source capacity', async () => {
|
|
2080
|
+
const amount = 1000n;
|
|
2081
|
+
const rawBalance = 1100n;
|
|
2082
|
+
const targetWithBuffer = 1050n;
|
|
2083
|
+
const route = createTestRoute({ amount });
|
|
2084
|
+
createTestIntent({ amount });
|
|
2085
|
+
inventoryRebalancer.setInventoryBalances({
|
|
2086
|
+
[SOLANA_CHAIN]: 0n,
|
|
2087
|
+
[ARBITRUM_CHAIN]: rawBalance,
|
|
2088
|
+
});
|
|
2089
|
+
bridge.quote
|
|
2090
|
+
.onFirstCall()
|
|
2091
|
+
.resolves(createMockBridgeQuote({
|
|
2092
|
+
fromAmount: rawBalance,
|
|
2093
|
+
toAmount: targetWithBuffer,
|
|
2094
|
+
toAmountMin: targetWithBuffer,
|
|
2095
|
+
}))
|
|
2096
|
+
.onSecondCall()
|
|
2097
|
+
.resolves(createMockBridgeQuote({
|
|
2098
|
+
fromAmount: rawBalance,
|
|
2099
|
+
toAmount: targetWithBuffer,
|
|
2100
|
+
toAmountMin: targetWithBuffer,
|
|
2101
|
+
}));
|
|
2102
|
+
bridge.execute.resolves({
|
|
2103
|
+
txHash: '0xForwardBoundaryBridgeTxHash',
|
|
2104
|
+
fromChain: 42161,
|
|
2105
|
+
toChain: 1399811149,
|
|
2106
|
+
});
|
|
2107
|
+
const results = await inventoryRebalancer.rebalance([route]);
|
|
2108
|
+
expect(results).to.have.lengthOf(1);
|
|
2109
|
+
expect(results[0].success).to.be.true;
|
|
2110
|
+
expect(bridge.execute.calledOnce).to.be.true;
|
|
2111
|
+
expect(bridge.quote.callCount).to.equal(2);
|
|
2112
|
+
expect(bridge.quote.getCall(1).args[0].fromAmount).to.equal(rawBalance);
|
|
2113
|
+
expect(bridge.quote.getCall(1).args[0].toAmount).to.be.undefined;
|
|
2114
|
+
});
|
|
2115
|
+
it('retries reverse quote as forward when exact-output exceeds source capacity', async () => {
|
|
2116
|
+
const amount = 1000n;
|
|
2117
|
+
const rawBalance = 1100n;
|
|
2118
|
+
const targetWithBuffer = 1050n;
|
|
2119
|
+
const route = createTestRoute({ amount });
|
|
2120
|
+
createTestIntent({ amount });
|
|
2121
|
+
inventoryRebalancer.setInventoryBalances({
|
|
2122
|
+
[SOLANA_CHAIN]: 0n,
|
|
2123
|
+
[ARBITRUM_CHAIN]: rawBalance,
|
|
2124
|
+
});
|
|
2125
|
+
bridge.quote
|
|
2126
|
+
.onFirstCall()
|
|
2127
|
+
.resolves(createMockBridgeQuote({
|
|
2128
|
+
fromAmount: rawBalance,
|
|
2129
|
+
toAmount: 1051n,
|
|
2130
|
+
toAmountMin: 1051n,
|
|
2131
|
+
}))
|
|
2132
|
+
.onSecondCall()
|
|
2133
|
+
.resolves(createMockBridgeQuote({
|
|
2134
|
+
fromAmount: rawBalance + 1n,
|
|
2135
|
+
toAmount: targetWithBuffer,
|
|
2136
|
+
toAmountMin: targetWithBuffer,
|
|
2137
|
+
}))
|
|
2138
|
+
.onThirdCall()
|
|
2139
|
+
.resolves(createMockBridgeQuote({
|
|
2140
|
+
fromAmount: rawBalance,
|
|
2141
|
+
toAmount: 1049n,
|
|
2142
|
+
toAmountMin: 1049n,
|
|
2143
|
+
}));
|
|
2144
|
+
bridge.execute.resolves({
|
|
2145
|
+
txHash: '0xForwardFallbackBridgeTxHash',
|
|
2146
|
+
fromChain: 42161,
|
|
2147
|
+
toChain: 1399811149,
|
|
2148
|
+
});
|
|
2149
|
+
const results = await inventoryRebalancer.rebalance([route]);
|
|
2150
|
+
expect(results).to.have.lengthOf(1);
|
|
2151
|
+
expect(results[0].success).to.be.true;
|
|
2152
|
+
expect(bridge.execute.calledOnce).to.be.true;
|
|
2153
|
+
expect(bridge.quote.callCount).to.equal(3);
|
|
2154
|
+
expect(bridge.quote.getCall(1).args[0].toAmount).to.equal(targetWithBuffer);
|
|
2155
|
+
expect(bridge.quote.getCall(2).args[0].fromAmount).to.equal(rawBalance);
|
|
2156
|
+
expect(bridge.quote.getCall(2).args[0].toAmount).to.be.undefined;
|
|
2157
|
+
});
|
|
1850
2158
|
it('handles quote failures gracefully by skipping the source chain', async () => {
|
|
1851
2159
|
// Setup: Native token where quote fails
|
|
1852
2160
|
const arbitrumToken = {
|