@t2000/sdk 0.17.22 → 0.17.23
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/README.md +1 -1
- package/dist/adapters/cetus.d.ts +29 -0
- package/dist/adapters/cetus.d.ts.map +1 -0
- package/dist/adapters/cetus.js +74 -0
- package/dist/adapters/cetus.js.map +1 -0
- package/dist/adapters/cetus.test.d.ts +2 -0
- package/dist/adapters/cetus.test.d.ts.map +1 -0
- package/dist/adapters/cetus.test.js +57 -0
- package/dist/adapters/cetus.test.js.map +1 -0
- package/dist/adapters/compliance.test.d.ts +8 -0
- package/dist/adapters/compliance.test.d.ts.map +1 -0
- package/dist/adapters/compliance.test.js +202 -0
- package/dist/adapters/compliance.test.js.map +1 -0
- package/dist/adapters/index.d.ts +10 -4
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +15 -2107
- package/dist/adapters/index.js.map +1 -1
- package/dist/adapters/navi.d.ts +41 -0
- package/dist/adapters/navi.d.ts.map +1 -0
- package/dist/adapters/navi.js +102 -0
- package/dist/adapters/navi.js.map +1 -0
- package/dist/adapters/navi.test.d.ts +2 -0
- package/dist/adapters/navi.test.d.ts.map +1 -0
- package/dist/adapters/navi.test.js +164 -0
- package/dist/adapters/navi.test.js.map +1 -0
- package/dist/adapters/registry.d.ts +47 -0
- package/dist/adapters/registry.d.ts.map +1 -0
- package/dist/adapters/registry.js +162 -0
- package/dist/adapters/registry.js.map +1 -0
- package/dist/adapters/registry.test.d.ts +2 -0
- package/dist/adapters/registry.test.d.ts.map +1 -0
- package/dist/adapters/registry.test.js +197 -0
- package/dist/adapters/registry.test.js.map +1 -0
- package/dist/adapters/suilend.d.ts +71 -0
- package/dist/adapters/suilend.d.ts.map +1 -0
- package/dist/adapters/suilend.js +826 -0
- package/dist/adapters/suilend.js.map +1 -0
- package/dist/adapters/suilend.test.d.ts +2 -0
- package/dist/adapters/suilend.test.d.ts.map +1 -0
- package/dist/adapters/suilend.test.js +294 -0
- package/dist/adapters/suilend.test.js.map +1 -0
- package/dist/adapters/types.d.ts +160 -0
- package/dist/adapters/types.d.ts.map +1 -0
- package/dist/adapters/types.js +2 -0
- package/dist/adapters/types.js.map +1 -0
- package/dist/auto-invest.d.ts +23 -0
- package/dist/auto-invest.d.ts.map +1 -0
- package/dist/auto-invest.js +131 -0
- package/dist/auto-invest.js.map +1 -0
- package/dist/auto-invest.test.d.ts +2 -0
- package/dist/auto-invest.test.d.ts.map +1 -0
- package/dist/auto-invest.test.js +220 -0
- package/dist/auto-invest.test.js.map +1 -0
- package/dist/constants.d.ts +177 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +135 -0
- package/dist/constants.js.map +1 -0
- package/dist/contacts.d.ts +25 -0
- package/dist/contacts.d.ts.map +1 -0
- package/dist/contacts.js +83 -0
- package/dist/contacts.js.map +1 -0
- package/dist/contacts.test.d.ts +2 -0
- package/dist/contacts.test.d.ts.map +1 -0
- package/dist/contacts.test.js +164 -0
- package/dist/contacts.test.js.map +1 -0
- package/dist/errors.d.ts +26 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +81 -0
- package/dist/errors.js.map +1 -0
- package/dist/errors.test.d.ts +2 -0
- package/dist/errors.test.d.ts.map +1 -0
- package/dist/errors.test.js +48 -0
- package/dist/errors.test.js.map +1 -0
- package/dist/gas/autoTopUp.d.ts +18 -0
- package/dist/gas/autoTopUp.d.ts.map +1 -0
- package/dist/gas/autoTopUp.js +55 -0
- package/dist/gas/autoTopUp.js.map +1 -0
- package/dist/gas/autoTopUp.test.d.ts +2 -0
- package/dist/gas/autoTopUp.test.d.ts.map +1 -0
- package/dist/gas/autoTopUp.test.js +59 -0
- package/dist/gas/autoTopUp.test.js.map +1 -0
- package/dist/gas/gasStation.d.ts +23 -0
- package/dist/gas/gasStation.d.ts.map +1 -0
- package/dist/gas/gasStation.js +58 -0
- package/dist/gas/gasStation.js.map +1 -0
- package/dist/gas/index.d.ts +4 -0
- package/dist/gas/index.d.ts.map +1 -0
- package/dist/gas/index.js +4 -0
- package/dist/gas/index.js.map +1 -0
- package/dist/gas/manager.d.ts +24 -0
- package/dist/gas/manager.d.ts.map +1 -0
- package/dist/gas/manager.js +142 -0
- package/dist/gas/manager.js.map +1 -0
- package/dist/gas/manager.test.d.ts +2 -0
- package/dist/gas/manager.test.d.ts.map +1 -0
- package/dist/gas/manager.test.js +220 -0
- package/dist/gas/manager.test.js.map +1 -0
- package/dist/gas/serialization.test.d.ts +2 -0
- package/dist/gas/serialization.test.d.ts.map +1 -0
- package/dist/gas/serialization.test.js +47 -0
- package/dist/gas/serialization.test.js.map +1 -0
- package/dist/index.d.ts +30 -649
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -6053
- package/dist/index.js.map +1 -1
- package/dist/invest.test.d.ts +2 -0
- package/dist/invest.test.d.ts.map +1 -0
- package/dist/invest.test.js +256 -0
- package/dist/invest.test.js.map +1 -0
- package/dist/portfolio.d.ts +39 -0
- package/dist/portfolio.d.ts.map +1 -0
- package/dist/portfolio.js +201 -0
- package/dist/portfolio.js.map +1 -0
- package/dist/portfolio.test.d.ts +2 -0
- package/dist/portfolio.test.d.ts.map +1 -0
- package/dist/portfolio.test.js +301 -0
- package/dist/portfolio.test.js.map +1 -0
- package/dist/protocols/cetus.d.ts +73 -0
- package/dist/protocols/cetus.d.ts.map +1 -0
- package/dist/protocols/cetus.js +267 -0
- package/dist/protocols/cetus.js.map +1 -0
- package/dist/protocols/cetus.test.d.ts +2 -0
- package/dist/protocols/cetus.test.d.ts.map +1 -0
- package/dist/protocols/cetus.test.js +325 -0
- package/dist/protocols/cetus.test.js.map +1 -0
- package/dist/protocols/navi.d.ts +59 -0
- package/dist/protocols/navi.d.ts.map +1 -0
- package/dist/protocols/navi.js +945 -0
- package/dist/protocols/navi.js.map +1 -0
- package/dist/protocols/navi.test.d.ts +2 -0
- package/dist/protocols/navi.test.d.ts.map +1 -0
- package/dist/protocols/navi.test.js +339 -0
- package/dist/protocols/navi.test.js.map +1 -0
- package/dist/protocols/protocolFee.d.ts +17 -0
- package/dist/protocols/protocolFee.d.ts.map +1 -0
- package/dist/protocols/protocolFee.js +62 -0
- package/dist/protocols/protocolFee.js.map +1 -0
- package/dist/protocols/protocolFee.test.d.ts +2 -0
- package/dist/protocols/protocolFee.test.d.ts.map +1 -0
- package/dist/protocols/protocolFee.test.js +137 -0
- package/dist/protocols/protocolFee.test.js.map +1 -0
- package/dist/protocols/sentinel.d.ts +18 -0
- package/dist/protocols/sentinel.d.ts.map +1 -0
- package/dist/protocols/sentinel.js +188 -0
- package/dist/protocols/sentinel.js.map +1 -0
- package/dist/protocols/sentinel.test.d.ts +2 -0
- package/dist/protocols/sentinel.test.d.ts.map +1 -0
- package/dist/protocols/sentinel.test.js +199 -0
- package/dist/protocols/sentinel.test.js.map +1 -0
- package/dist/protocols/yieldTracker.d.ts +6 -0
- package/dist/protocols/yieldTracker.d.ts.map +1 -0
- package/dist/protocols/yieldTracker.js +29 -0
- package/dist/protocols/yieldTracker.js.map +1 -0
- package/dist/safeguards/enforcer.d.ts +18 -0
- package/dist/safeguards/enforcer.d.ts.map +1 -0
- package/dist/safeguards/enforcer.js +130 -0
- package/dist/safeguards/enforcer.js.map +1 -0
- package/dist/safeguards/enforcer.test.d.ts +2 -0
- package/dist/safeguards/enforcer.test.d.ts.map +1 -0
- package/dist/safeguards/enforcer.test.js +212 -0
- package/dist/safeguards/enforcer.test.js.map +1 -0
- package/dist/safeguards/errors.d.ts +24 -0
- package/dist/safeguards/errors.d.ts.map +1 -0
- package/dist/safeguards/errors.js +31 -0
- package/dist/safeguards/errors.js.map +1 -0
- package/dist/safeguards/index.d.ts +6 -0
- package/dist/safeguards/index.d.ts.map +1 -0
- package/dist/safeguards/index.js +4 -0
- package/dist/safeguards/index.js.map +1 -0
- package/dist/safeguards/types.d.ts +16 -0
- package/dist/safeguards/types.d.ts.map +1 -0
- package/dist/safeguards/types.js +13 -0
- package/dist/safeguards/types.js.map +1 -0
- package/dist/strategy.d.ts +22 -0
- package/dist/strategy.d.ts.map +1 -0
- package/dist/strategy.js +113 -0
- package/dist/strategy.js.map +1 -0
- package/dist/strategy.test.d.ts +2 -0
- package/dist/strategy.test.d.ts.map +1 -0
- package/dist/strategy.test.js +212 -0
- package/dist/strategy.test.js.map +1 -0
- package/dist/t2000.d.ts +218 -0
- package/dist/t2000.d.ts.map +1 -0
- package/dist/t2000.integration.test.d.ts +2 -0
- package/dist/t2000.integration.test.d.ts.map +1 -0
- package/dist/t2000.integration.test.js +954 -0
- package/dist/t2000.integration.test.js.map +1 -0
- package/dist/t2000.js +2395 -0
- package/dist/t2000.js.map +1 -0
- package/dist/types.d.ts +419 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/format.d.ts +22 -0
- package/dist/utils/format.d.ts.map +1 -0
- package/dist/utils/format.js +71 -0
- package/dist/utils/format.js.map +1 -0
- package/dist/utils/format.test.d.ts +2 -0
- package/dist/utils/format.test.d.ts.map +1 -0
- package/dist/utils/format.test.js +187 -0
- package/dist/utils/format.test.js.map +1 -0
- package/dist/utils/hashcash.d.ts +2 -0
- package/dist/utils/hashcash.d.ts.map +1 -0
- package/dist/utils/hashcash.js +27 -0
- package/dist/utils/hashcash.js.map +1 -0
- package/dist/utils/hashcash.test.d.ts +2 -0
- package/dist/utils/hashcash.test.d.ts.map +1 -0
- package/dist/utils/hashcash.test.js +40 -0
- package/dist/utils/hashcash.test.js.map +1 -0
- package/dist/utils/retry.d.ts +9 -0
- package/dist/utils/retry.d.ts.map +1 -0
- package/dist/utils/retry.js +47 -0
- package/dist/utils/retry.js.map +1 -0
- package/dist/utils/simulate.d.ts +15 -0
- package/dist/utils/simulate.d.ts.map +1 -0
- package/dist/utils/simulate.js +75 -0
- package/dist/utils/simulate.js.map +1 -0
- package/dist/utils/simulate.test.d.ts +2 -0
- package/dist/utils/simulate.test.d.ts.map +1 -0
- package/dist/utils/simulate.test.js +80 -0
- package/dist/utils/simulate.test.js.map +1 -0
- package/dist/utils/sui.d.ts +6 -0
- package/dist/utils/sui.d.ts.map +1 -0
- package/dist/utils/sui.js +28 -0
- package/dist/utils/sui.js.map +1 -0
- package/dist/utils/sui.test.d.ts +2 -0
- package/dist/utils/sui.test.d.ts.map +1 -0
- package/dist/utils/sui.test.js +58 -0
- package/dist/utils/sui.test.js.map +1 -0
- package/dist/wallet/balance.d.ts +4 -0
- package/dist/wallet/balance.d.ts.map +1 -0
- package/dist/wallet/balance.js +98 -0
- package/dist/wallet/balance.js.map +1 -0
- package/dist/wallet/history.d.ts +4 -0
- package/dist/wallet/history.d.ts.map +1 -0
- package/dist/wallet/history.js +38 -0
- package/dist/wallet/history.js.map +1 -0
- package/dist/wallet/keyManager.d.ts +9 -0
- package/dist/wallet/keyManager.d.ts.map +1 -0
- package/dist/wallet/keyManager.js +113 -0
- package/dist/wallet/keyManager.js.map +1 -0
- package/dist/wallet/keyManager.test.d.ts +2 -0
- package/dist/wallet/keyManager.test.d.ts.map +1 -0
- package/dist/wallet/keyManager.test.js +55 -0
- package/dist/wallet/keyManager.test.js.map +1 -0
- package/dist/wallet/send.d.ts +24 -0
- package/dist/wallet/send.d.ts.map +1 -0
- package/dist/wallet/send.js +95 -0
- package/dist/wallet/send.js.map +1 -0
- package/dist/wallet/send.test.d.ts +2 -0
- package/dist/wallet/send.test.d.ts.map +1 -0
- package/dist/wallet/send.test.js +69 -0
- package/dist/wallet/send.test.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,954 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for T2000 multi-protocol orchestration.
|
|
3
|
+
*
|
|
4
|
+
* These tests exercise the T2000 class methods (save, withdraw, etc.)
|
|
5
|
+
* with multiple mock adapters registered, verifying correct routing,
|
|
6
|
+
* validation, and edge-case handling that unit tests miss.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
9
|
+
import { Transaction } from '@mysten/sui/transactions';
|
|
10
|
+
import { ProtocolRegistry } from './adapters/registry.js';
|
|
11
|
+
function createMockLending(overrides) {
|
|
12
|
+
return {
|
|
13
|
+
version: '1.0.0',
|
|
14
|
+
capabilities: ['save', 'withdraw', 'borrow', 'repay'],
|
|
15
|
+
supportedAssets: ['USDC'],
|
|
16
|
+
supportsSameAssetBorrow: true,
|
|
17
|
+
init: vi.fn(),
|
|
18
|
+
getRates: vi.fn().mockResolvedValue({ asset: 'USDC', saveApy: 5.0, borrowApy: 3.0 }),
|
|
19
|
+
getPositions: vi.fn().mockResolvedValue({ supplies: [], borrows: [] }),
|
|
20
|
+
getHealth: vi.fn().mockResolvedValue({ healthFactor: 10, supplied: 0, borrowed: 0, maxBorrow: 0, liquidationThreshold: 0.8 }),
|
|
21
|
+
buildSaveTx: vi.fn().mockResolvedValue({ tx: new Transaction() }),
|
|
22
|
+
buildWithdrawTx: vi.fn().mockResolvedValue({ tx: new Transaction(), effectiveAmount: 10 }),
|
|
23
|
+
buildBorrowTx: vi.fn().mockResolvedValue({ tx: new Transaction() }),
|
|
24
|
+
buildRepayTx: vi.fn().mockResolvedValue({ tx: new Transaction() }),
|
|
25
|
+
maxWithdraw: vi.fn().mockResolvedValue({ maxAmount: 0, healthFactorAfter: 999, currentHF: 999 }),
|
|
26
|
+
maxBorrow: vi.fn().mockResolvedValue({ maxAmount: 0, healthFactorAfter: 999, currentHF: 999 }),
|
|
27
|
+
...overrides,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
// ─── withdraw all: multi-protocol orchestration ───────────────
|
|
31
|
+
describe('withdraw all — multi-protocol', () => {
|
|
32
|
+
let navi;
|
|
33
|
+
let suilend;
|
|
34
|
+
let registry;
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
navi = createMockLending({
|
|
37
|
+
id: 'navi',
|
|
38
|
+
name: 'NAVI Protocol',
|
|
39
|
+
getPositions: vi.fn().mockResolvedValue({
|
|
40
|
+
supplies: [{ asset: 'USDC', amount: 5.0, apy: 5.5 }],
|
|
41
|
+
borrows: [],
|
|
42
|
+
}),
|
|
43
|
+
maxWithdraw: vi.fn().mockResolvedValue({ maxAmount: 5.0, healthFactorAfter: 999, currentHF: 999 }),
|
|
44
|
+
buildWithdrawTx: vi.fn().mockResolvedValue({ tx: new Transaction(), effectiveAmount: 5.0 }),
|
|
45
|
+
});
|
|
46
|
+
suilend = createMockLending({
|
|
47
|
+
id: 'suilend',
|
|
48
|
+
name: 'Suilend',
|
|
49
|
+
supportsSameAssetBorrow: false,
|
|
50
|
+
capabilities: ['save', 'withdraw'],
|
|
51
|
+
getPositions: vi.fn().mockResolvedValue({
|
|
52
|
+
supplies: [{ asset: 'USDC', amount: 2.5, apy: 2.2 }],
|
|
53
|
+
borrows: [],
|
|
54
|
+
}),
|
|
55
|
+
maxWithdraw: vi.fn().mockResolvedValue({ maxAmount: 2.5, healthFactorAfter: 999, currentHF: 999 }),
|
|
56
|
+
buildWithdrawTx: vi.fn().mockResolvedValue({ tx: new Transaction(), effectiveAmount: 2.5 }),
|
|
57
|
+
});
|
|
58
|
+
registry = new ProtocolRegistry();
|
|
59
|
+
registry.registerLending(navi);
|
|
60
|
+
registry.registerLending(suilend);
|
|
61
|
+
});
|
|
62
|
+
it('allPositions returns positions from both protocols', async () => {
|
|
63
|
+
const positions = await registry.allPositions('0xtest');
|
|
64
|
+
expect(positions).toHaveLength(2);
|
|
65
|
+
expect(positions.map(p => p.protocolId).sort()).toEqual(['navi', 'suilend']);
|
|
66
|
+
});
|
|
67
|
+
it('allPositions filters out protocols with no supply', async () => {
|
|
68
|
+
navi.getPositions.mockResolvedValue({ supplies: [], borrows: [] });
|
|
69
|
+
const positions = await registry.allPositions('0xtest');
|
|
70
|
+
expect(positions).toHaveLength(1);
|
|
71
|
+
expect(positions[0].protocolId).toBe('suilend');
|
|
72
|
+
});
|
|
73
|
+
it('allPositions returns empty when no protocol has supply', async () => {
|
|
74
|
+
navi.getPositions.mockResolvedValue({ supplies: [], borrows: [] });
|
|
75
|
+
suilend.getPositions.mockResolvedValue({ supplies: [], borrows: [] });
|
|
76
|
+
const positions = await registry.allPositions('0xtest');
|
|
77
|
+
expect(positions).toHaveLength(0);
|
|
78
|
+
});
|
|
79
|
+
it('getLending returns correct adapter by id', () => {
|
|
80
|
+
expect(registry.getLending('navi')).toBe(navi);
|
|
81
|
+
expect(registry.getLending('suilend')).toBe(suilend);
|
|
82
|
+
expect(registry.getLending('unknown')).toBeUndefined();
|
|
83
|
+
});
|
|
84
|
+
it('maxWithdraw returns correct amounts from each adapter', async () => {
|
|
85
|
+
const naviMax = await navi.maxWithdraw('0xtest', 'USDC');
|
|
86
|
+
expect(naviMax.maxAmount).toBe(5.0);
|
|
87
|
+
const suilendMax = await suilend.maxWithdraw('0xtest', 'USDC');
|
|
88
|
+
expect(suilendMax.maxAmount).toBe(2.5);
|
|
89
|
+
});
|
|
90
|
+
it('skips protocols with zero balance in withdraw all flow', async () => {
|
|
91
|
+
// NAVI has a dust position ($0.0001) — below the $0.001 threshold
|
|
92
|
+
navi.getPositions.mockResolvedValue({
|
|
93
|
+
supplies: [{ asset: 'USDC', amount: 0.0001, apy: 5.5 }],
|
|
94
|
+
borrows: [],
|
|
95
|
+
});
|
|
96
|
+
navi.maxWithdraw.mockResolvedValue({ maxAmount: 0.0001, healthFactorAfter: 999, currentHF: 999 });
|
|
97
|
+
const positions = await registry.allPositions('0xtest');
|
|
98
|
+
// allPositions includes it (any non-empty), but withdraw all filtering skips dust
|
|
99
|
+
const withSupply = positions.filter(p => p.positions.supplies.some(s => s.asset === 'USDC' && s.amount > 0.001));
|
|
100
|
+
expect(withSupply).toHaveLength(1);
|
|
101
|
+
expect(withSupply[0].protocolId).toBe('suilend');
|
|
102
|
+
});
|
|
103
|
+
it('handles protocol that throws during getPositions', async () => {
|
|
104
|
+
navi.getPositions.mockRejectedValue(new Error('rpc fail'));
|
|
105
|
+
const positions = await registry.allPositions('0xtest');
|
|
106
|
+
expect(positions).toHaveLength(1);
|
|
107
|
+
expect(positions[0].protocolId).toBe('suilend');
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
// ─── save: balance validation ───────────────────────────────
|
|
111
|
+
describe('save — balance validation', () => {
|
|
112
|
+
let registry;
|
|
113
|
+
beforeEach(() => {
|
|
114
|
+
const navi = createMockLending({
|
|
115
|
+
id: 'navi',
|
|
116
|
+
name: 'NAVI Protocol',
|
|
117
|
+
getRates: vi.fn().mockResolvedValue({ asset: 'USDC', saveApy: 5.5, borrowApy: 8.0 }),
|
|
118
|
+
});
|
|
119
|
+
registry = new ProtocolRegistry();
|
|
120
|
+
registry.registerLending(navi);
|
|
121
|
+
});
|
|
122
|
+
it('bestSaveRate returns the best adapter', async () => {
|
|
123
|
+
const suilend = createMockLending({
|
|
124
|
+
id: 'suilend',
|
|
125
|
+
name: 'Suilend',
|
|
126
|
+
getRates: vi.fn().mockResolvedValue({ asset: 'USDC', saveApy: 2.2, borrowApy: 5.5 }),
|
|
127
|
+
});
|
|
128
|
+
registry.registerLending(suilend);
|
|
129
|
+
const result = await registry.bestSaveRate('USDC');
|
|
130
|
+
expect(result.adapter.id).toBe('navi');
|
|
131
|
+
expect(result.rate.saveApy).toBe(5.5);
|
|
132
|
+
});
|
|
133
|
+
it('bestSaveRate picks suilend when it has higher rate', async () => {
|
|
134
|
+
const suilend = createMockLending({
|
|
135
|
+
id: 'suilend',
|
|
136
|
+
name: 'Suilend',
|
|
137
|
+
getRates: vi.fn().mockResolvedValue({ asset: 'USDC', saveApy: 8.0, borrowApy: 5.5 }),
|
|
138
|
+
});
|
|
139
|
+
registry.registerLending(suilend);
|
|
140
|
+
const result = await registry.bestSaveRate('USDC');
|
|
141
|
+
expect(result.adapter.id).toBe('suilend');
|
|
142
|
+
expect(result.rate.saveApy).toBe(8.0);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
// ─── withdraw: specific protocol routing ───────────────────
|
|
146
|
+
describe('withdraw — protocol routing', () => {
|
|
147
|
+
let navi;
|
|
148
|
+
let suilend;
|
|
149
|
+
let registry;
|
|
150
|
+
beforeEach(() => {
|
|
151
|
+
navi = createMockLending({
|
|
152
|
+
id: 'navi',
|
|
153
|
+
name: 'NAVI Protocol',
|
|
154
|
+
getPositions: vi.fn().mockResolvedValue({
|
|
155
|
+
supplies: [{ asset: 'USDC', amount: 1.0, apy: 5.5 }],
|
|
156
|
+
borrows: [],
|
|
157
|
+
}),
|
|
158
|
+
maxWithdraw: vi.fn().mockResolvedValue({ maxAmount: 1.0, healthFactorAfter: 999, currentHF: 999 }),
|
|
159
|
+
});
|
|
160
|
+
suilend = createMockLending({
|
|
161
|
+
id: 'suilend',
|
|
162
|
+
name: 'Suilend',
|
|
163
|
+
supportsSameAssetBorrow: false,
|
|
164
|
+
capabilities: ['save', 'withdraw'],
|
|
165
|
+
getPositions: vi.fn().mockResolvedValue({
|
|
166
|
+
supplies: [{ asset: 'USDC', amount: 10.0, apy: 2.2 }],
|
|
167
|
+
borrows: [],
|
|
168
|
+
}),
|
|
169
|
+
maxWithdraw: vi.fn().mockResolvedValue({ maxAmount: 10.0, healthFactorAfter: 999, currentHF: 999 }),
|
|
170
|
+
});
|
|
171
|
+
registry = new ProtocolRegistry();
|
|
172
|
+
registry.registerLending(navi);
|
|
173
|
+
registry.registerLending(suilend);
|
|
174
|
+
});
|
|
175
|
+
it('getLending("suilend") returns suilend adapter', () => {
|
|
176
|
+
expect(registry.getLending('suilend')).toBe(suilend);
|
|
177
|
+
});
|
|
178
|
+
it('getLending("navi") returns navi adapter', () => {
|
|
179
|
+
expect(registry.getLending('navi')).toBe(navi);
|
|
180
|
+
});
|
|
181
|
+
it('withdraw with --protocol suilend routes to suilend', () => {
|
|
182
|
+
const adapter = registry.getLending('suilend');
|
|
183
|
+
expect(adapter).toBe(suilend);
|
|
184
|
+
expect(adapter?.id).toBe('suilend');
|
|
185
|
+
});
|
|
186
|
+
it('allPositions sums across both protocols', async () => {
|
|
187
|
+
const positions = await registry.allPositions('0xtest');
|
|
188
|
+
const totalSupplied = positions.reduce((sum, p) => sum + p.positions.supplies.reduce((s, sup) => s + sup.amount, 0), 0);
|
|
189
|
+
expect(totalSupplied).toBe(11.0);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
// ─── edge cases ─────────────────────────────────────────────
|
|
193
|
+
describe('multi-protocol edge cases', () => {
|
|
194
|
+
it('registry with single protocol still works for allPositions', async () => {
|
|
195
|
+
const registry = new ProtocolRegistry();
|
|
196
|
+
const navi = createMockLending({
|
|
197
|
+
id: 'navi',
|
|
198
|
+
name: 'NAVI Protocol',
|
|
199
|
+
getPositions: vi.fn().mockResolvedValue({
|
|
200
|
+
supplies: [{ asset: 'USDC', amount: 5.0, apy: 5.5 }],
|
|
201
|
+
borrows: [],
|
|
202
|
+
}),
|
|
203
|
+
});
|
|
204
|
+
registry.registerLending(navi);
|
|
205
|
+
const positions = await registry.allPositions('0xtest');
|
|
206
|
+
expect(positions).toHaveLength(1);
|
|
207
|
+
expect(positions[0].protocolId).toBe('navi');
|
|
208
|
+
});
|
|
209
|
+
it('allPositions with 3+ protocols returns all with supply', async () => {
|
|
210
|
+
const registry = new ProtocolRegistry();
|
|
211
|
+
const a = createMockLending({
|
|
212
|
+
id: 'proto-a',
|
|
213
|
+
name: 'Proto A',
|
|
214
|
+
getPositions: vi.fn().mockResolvedValue({
|
|
215
|
+
supplies: [{ asset: 'USDC', amount: 1.0, apy: 3.0 }],
|
|
216
|
+
borrows: [],
|
|
217
|
+
}),
|
|
218
|
+
});
|
|
219
|
+
const b = createMockLending({
|
|
220
|
+
id: 'proto-b',
|
|
221
|
+
name: 'Proto B',
|
|
222
|
+
getPositions: vi.fn().mockResolvedValue({ supplies: [], borrows: [] }),
|
|
223
|
+
});
|
|
224
|
+
const c = createMockLending({
|
|
225
|
+
id: 'proto-c',
|
|
226
|
+
name: 'Proto C',
|
|
227
|
+
getPositions: vi.fn().mockResolvedValue({
|
|
228
|
+
supplies: [{ asset: 'USDC', amount: 3.0, apy: 7.0 }],
|
|
229
|
+
borrows: [],
|
|
230
|
+
}),
|
|
231
|
+
});
|
|
232
|
+
registry.registerLending(a);
|
|
233
|
+
registry.registerLending(b);
|
|
234
|
+
registry.registerLending(c);
|
|
235
|
+
const positions = await registry.allPositions('0xtest');
|
|
236
|
+
expect(positions).toHaveLength(2);
|
|
237
|
+
expect(positions.map(p => p.protocolId).sort()).toEqual(['proto-a', 'proto-c']);
|
|
238
|
+
});
|
|
239
|
+
it('tiny balance (<0.001) is treated as empty for withdraw all filtering', async () => {
|
|
240
|
+
const registry = new ProtocolRegistry();
|
|
241
|
+
const navi = createMockLending({
|
|
242
|
+
id: 'navi',
|
|
243
|
+
name: 'NAVI Protocol',
|
|
244
|
+
getPositions: vi.fn().mockResolvedValue({
|
|
245
|
+
supplies: [{ asset: 'USDC', amount: 0.0001, apy: 5.5 }],
|
|
246
|
+
borrows: [],
|
|
247
|
+
}),
|
|
248
|
+
maxWithdraw: vi.fn().mockResolvedValue({ maxAmount: 0.0001, healthFactorAfter: 999, currentHF: 999 }),
|
|
249
|
+
});
|
|
250
|
+
registry.registerLending(navi);
|
|
251
|
+
const positions = await registry.allPositions('0xtest');
|
|
252
|
+
// allPositions returns it (>0 supplies), but withdraw all filtering should skip it
|
|
253
|
+
const withSupply = positions.filter(p => p.positions.supplies.some(s => s.asset === 'USDC' && s.amount > 0.001));
|
|
254
|
+
expect(withSupply).toHaveLength(0);
|
|
255
|
+
});
|
|
256
|
+
it('allRates returns rates from all registered protocols', async () => {
|
|
257
|
+
const registry = new ProtocolRegistry();
|
|
258
|
+
const navi = createMockLending({
|
|
259
|
+
id: 'navi',
|
|
260
|
+
name: 'NAVI',
|
|
261
|
+
getRates: vi.fn().mockResolvedValue({ asset: 'USDC', saveApy: 5.5, borrowApy: 8.0 }),
|
|
262
|
+
});
|
|
263
|
+
const suilend = createMockLending({
|
|
264
|
+
id: 'suilend',
|
|
265
|
+
name: 'Suilend',
|
|
266
|
+
getRates: vi.fn().mockResolvedValue({ asset: 'USDC', saveApy: 2.2, borrowApy: 5.5 }),
|
|
267
|
+
});
|
|
268
|
+
registry.registerLending(navi);
|
|
269
|
+
registry.registerLending(suilend);
|
|
270
|
+
const rates = await registry.allRates('USDC');
|
|
271
|
+
expect(rates).toHaveLength(2);
|
|
272
|
+
expect(rates.find(r => r.protocolId === 'navi')?.rates.saveApy).toBe(5.5);
|
|
273
|
+
expect(rates.find(r => r.protocolId === 'suilend')?.rates.saveApy).toBe(2.2);
|
|
274
|
+
});
|
|
275
|
+
it('allRates skips protocol that throws', async () => {
|
|
276
|
+
const registry = new ProtocolRegistry();
|
|
277
|
+
const broken = createMockLending({
|
|
278
|
+
id: 'broken',
|
|
279
|
+
name: 'Broken',
|
|
280
|
+
getRates: vi.fn().mockRejectedValue(new Error('rpc fail')),
|
|
281
|
+
});
|
|
282
|
+
const good = createMockLending({
|
|
283
|
+
id: 'good',
|
|
284
|
+
name: 'Good',
|
|
285
|
+
getRates: vi.fn().mockResolvedValue({ asset: 'USDC', saveApy: 4.0, borrowApy: 6.0 }),
|
|
286
|
+
});
|
|
287
|
+
registry.registerLending(broken);
|
|
288
|
+
registry.registerLending(good);
|
|
289
|
+
const rates = await registry.allRates('USDC');
|
|
290
|
+
expect(rates).toHaveLength(1);
|
|
291
|
+
expect(rates[0].protocolId).toBe('good');
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
// ─── borrow/repay routing with supportsSameAssetBorrow ────────
|
|
295
|
+
describe('borrow routing — supportsSameAssetBorrow', () => {
|
|
296
|
+
let navi;
|
|
297
|
+
let suilend;
|
|
298
|
+
let registry;
|
|
299
|
+
beforeEach(() => {
|
|
300
|
+
navi = createMockLending({
|
|
301
|
+
id: 'navi',
|
|
302
|
+
name: 'NAVI Protocol',
|
|
303
|
+
supportsSameAssetBorrow: true,
|
|
304
|
+
getRates: vi.fn().mockResolvedValue({ asset: 'USDC', saveApy: 5.5, borrowApy: 8.0 }),
|
|
305
|
+
});
|
|
306
|
+
suilend = createMockLending({
|
|
307
|
+
id: 'suilend',
|
|
308
|
+
name: 'Suilend',
|
|
309
|
+
supportsSameAssetBorrow: false,
|
|
310
|
+
capabilities: ['save', 'withdraw'],
|
|
311
|
+
getRates: vi.fn().mockResolvedValue({ asset: 'USDC', saveApy: 2.2, borrowApy: 5.5 }),
|
|
312
|
+
});
|
|
313
|
+
registry = new ProtocolRegistry();
|
|
314
|
+
registry.registerLending(navi);
|
|
315
|
+
registry.registerLending(suilend);
|
|
316
|
+
});
|
|
317
|
+
it('bestBorrowRate skips adapters without same-asset borrow', async () => {
|
|
318
|
+
const result = await registry.bestBorrowRate('USDC', { requireSameAssetBorrow: true });
|
|
319
|
+
expect(result.adapter.id).toBe('navi');
|
|
320
|
+
});
|
|
321
|
+
it('bestBorrowRate throws when no adapter supports borrow', async () => {
|
|
322
|
+
const registry = new ProtocolRegistry();
|
|
323
|
+
const saveOnly = createMockLending({
|
|
324
|
+
id: 'save-only',
|
|
325
|
+
name: 'Save Only',
|
|
326
|
+
capabilities: ['save', 'withdraw'],
|
|
327
|
+
supportsSameAssetBorrow: false,
|
|
328
|
+
});
|
|
329
|
+
registry.registerLending(saveOnly);
|
|
330
|
+
await expect(registry.bestBorrowRate('USDC')).rejects.toThrow('No lending adapter supports borrowing USDC');
|
|
331
|
+
});
|
|
332
|
+
it('listLending filtered by borrow capability excludes save-only adapters', () => {
|
|
333
|
+
const borrowable = registry.listLending().filter(a => a.supportedAssets.includes('USDC') &&
|
|
334
|
+
a.capabilities.includes('borrow') &&
|
|
335
|
+
a.supportsSameAssetBorrow);
|
|
336
|
+
expect(borrowable).toHaveLength(1);
|
|
337
|
+
expect(borrowable[0].id).toBe('navi');
|
|
338
|
+
});
|
|
339
|
+
it('listLending filtered by repay capability excludes save-only adapters', () => {
|
|
340
|
+
const repayable = registry.listLending().filter(a => a.supportedAssets.includes('USDC') && a.capabilities.includes('repay'));
|
|
341
|
+
expect(repayable).toHaveLength(1);
|
|
342
|
+
expect(repayable[0].id).toBe('navi');
|
|
343
|
+
});
|
|
344
|
+
it('bestBorrowRate picks lowest APY among eligible', async () => {
|
|
345
|
+
const cheapBorrow = createMockLending({
|
|
346
|
+
id: 'cheap',
|
|
347
|
+
name: 'Cheap Borrow',
|
|
348
|
+
supportsSameAssetBorrow: true,
|
|
349
|
+
getRates: vi.fn().mockResolvedValue({ asset: 'USDC', saveApy: 3.0, borrowApy: 2.0 }),
|
|
350
|
+
});
|
|
351
|
+
registry.registerLending(cheapBorrow);
|
|
352
|
+
const result = await registry.bestBorrowRate('USDC');
|
|
353
|
+
expect(result.adapter.id).toBe('cheap');
|
|
354
|
+
expect(result.rate.borrowApy).toBe(2.0);
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
// ─── positions() flattening across protocols ──────────────────
|
|
358
|
+
describe('positions — multi-protocol flattening', () => {
|
|
359
|
+
it('flattens supplies from multiple protocols with correct protocol field', async () => {
|
|
360
|
+
const registry = new ProtocolRegistry();
|
|
361
|
+
const navi = createMockLending({
|
|
362
|
+
id: 'navi',
|
|
363
|
+
name: 'NAVI Protocol',
|
|
364
|
+
getPositions: vi.fn().mockResolvedValue({
|
|
365
|
+
supplies: [{ asset: 'USDC', amount: 5.0, apy: 5.5 }],
|
|
366
|
+
borrows: [],
|
|
367
|
+
}),
|
|
368
|
+
});
|
|
369
|
+
const suilend = createMockLending({
|
|
370
|
+
id: 'suilend',
|
|
371
|
+
name: 'Suilend',
|
|
372
|
+
getPositions: vi.fn().mockResolvedValue({
|
|
373
|
+
supplies: [{ asset: 'USDC', amount: 2.5, apy: 2.2 }],
|
|
374
|
+
borrows: [],
|
|
375
|
+
}),
|
|
376
|
+
});
|
|
377
|
+
registry.registerLending(navi);
|
|
378
|
+
registry.registerLending(suilend);
|
|
379
|
+
const allPos = await registry.allPositions('0xtest');
|
|
380
|
+
const flattened = allPos.flatMap(p => [
|
|
381
|
+
...p.positions.supplies.map(s => ({
|
|
382
|
+
protocol: p.protocolId,
|
|
383
|
+
asset: s.asset,
|
|
384
|
+
type: 'save',
|
|
385
|
+
amount: s.amount,
|
|
386
|
+
apy: s.apy,
|
|
387
|
+
})),
|
|
388
|
+
...p.positions.borrows.map(b => ({
|
|
389
|
+
protocol: p.protocolId,
|
|
390
|
+
asset: b.asset,
|
|
391
|
+
type: 'borrow',
|
|
392
|
+
amount: b.amount,
|
|
393
|
+
apy: b.apy,
|
|
394
|
+
})),
|
|
395
|
+
]);
|
|
396
|
+
expect(flattened).toHaveLength(2);
|
|
397
|
+
expect(flattened.find(p => p.protocol === 'navi')?.amount).toBe(5.0);
|
|
398
|
+
expect(flattened.find(p => p.protocol === 'suilend')?.amount).toBe(2.5);
|
|
399
|
+
});
|
|
400
|
+
it('includes both supplies and borrows in flattened result', async () => {
|
|
401
|
+
const registry = new ProtocolRegistry();
|
|
402
|
+
const navi = createMockLending({
|
|
403
|
+
id: 'navi',
|
|
404
|
+
name: 'NAVI Protocol',
|
|
405
|
+
getPositions: vi.fn().mockResolvedValue({
|
|
406
|
+
supplies: [{ asset: 'USDC', amount: 10.0, apy: 5.5 }],
|
|
407
|
+
borrows: [{ asset: 'USDC', amount: 3.0, apy: 8.0 }],
|
|
408
|
+
}),
|
|
409
|
+
});
|
|
410
|
+
registry.registerLending(navi);
|
|
411
|
+
const allPos = await registry.allPositions('0xtest');
|
|
412
|
+
const flattened = allPos.flatMap(p => [
|
|
413
|
+
...p.positions.supplies.map(s => ({ protocol: p.protocolId, type: 'save', amount: s.amount })),
|
|
414
|
+
...p.positions.borrows.map(b => ({ protocol: p.protocolId, type: 'borrow', amount: b.amount })),
|
|
415
|
+
]);
|
|
416
|
+
expect(flattened).toHaveLength(2);
|
|
417
|
+
expect(flattened.find(p => p.type === 'save')?.amount).toBe(10.0);
|
|
418
|
+
expect(flattened.find(p => p.type === 'borrow')?.amount).toBe(3.0);
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
// ─── withdraw all --protocol (single protocol "all") ──────────
|
|
422
|
+
describe('withdraw all --protocol (single protocol path)', () => {
|
|
423
|
+
it('withdraw all --protocol suilend only touches suilend adapter', async () => {
|
|
424
|
+
const registry = new ProtocolRegistry();
|
|
425
|
+
const navi = createMockLending({
|
|
426
|
+
id: 'navi',
|
|
427
|
+
name: 'NAVI Protocol',
|
|
428
|
+
getPositions: vi.fn().mockResolvedValue({
|
|
429
|
+
supplies: [{ asset: 'USDC', amount: 5.0, apy: 5.5 }],
|
|
430
|
+
borrows: [],
|
|
431
|
+
}),
|
|
432
|
+
maxWithdraw: vi.fn().mockResolvedValue({ maxAmount: 5.0, healthFactorAfter: 999, currentHF: 999 }),
|
|
433
|
+
});
|
|
434
|
+
const suilend = createMockLending({
|
|
435
|
+
id: 'suilend',
|
|
436
|
+
name: 'Suilend',
|
|
437
|
+
getPositions: vi.fn().mockResolvedValue({
|
|
438
|
+
supplies: [{ asset: 'USDC', amount: 2.5, apy: 2.2 }],
|
|
439
|
+
borrows: [],
|
|
440
|
+
}),
|
|
441
|
+
maxWithdraw: vi.fn().mockResolvedValue({ maxAmount: 2.5, healthFactorAfter: 999, currentHF: 999 }),
|
|
442
|
+
});
|
|
443
|
+
registry.registerLending(navi);
|
|
444
|
+
registry.registerLending(suilend);
|
|
445
|
+
// When protocol is specified, getLending returns that specific adapter
|
|
446
|
+
const adapter = registry.getLending('suilend');
|
|
447
|
+
expect(adapter).toBe(suilend);
|
|
448
|
+
const max = await adapter.maxWithdraw('0xtest', 'USDC');
|
|
449
|
+
expect(max.maxAmount).toBe(2.5);
|
|
450
|
+
// NAVI should not be touched
|
|
451
|
+
expect(navi.maxWithdraw).not.toHaveBeenCalled();
|
|
452
|
+
});
|
|
453
|
+
});
|
|
454
|
+
// ─── withdraw with active borrows (health factor guard) ───────
|
|
455
|
+
describe('withdraw — health factor guard', () => {
|
|
456
|
+
it('maxWithdraw respects health factor when borrowed > 0', async () => {
|
|
457
|
+
const registry = new ProtocolRegistry();
|
|
458
|
+
const navi = createMockLending({
|
|
459
|
+
id: 'navi',
|
|
460
|
+
name: 'NAVI Protocol',
|
|
461
|
+
getHealth: vi.fn().mockResolvedValue({
|
|
462
|
+
healthFactor: 2.5,
|
|
463
|
+
supplied: 100,
|
|
464
|
+
borrowed: 50,
|
|
465
|
+
maxBorrow: 30,
|
|
466
|
+
liquidationThreshold: 0.8,
|
|
467
|
+
}),
|
|
468
|
+
maxWithdraw: vi.fn().mockResolvedValue({ maxAmount: 30.0, healthFactorAfter: 1.5, currentHF: 2.5 }),
|
|
469
|
+
});
|
|
470
|
+
registry.registerLending(navi);
|
|
471
|
+
const adapter = registry.getLending('navi');
|
|
472
|
+
const hf = await adapter.getHealth('0xtest');
|
|
473
|
+
expect(hf.borrowed).toBe(50);
|
|
474
|
+
expect(hf.healthFactor).toBe(2.5);
|
|
475
|
+
const max = await adapter.maxWithdraw('0xtest', 'USDC');
|
|
476
|
+
expect(max.maxAmount).toBe(30.0);
|
|
477
|
+
expect(max.healthFactorAfter).toBe(1.5);
|
|
478
|
+
});
|
|
479
|
+
it('protocol with no borrows allows full withdrawal', async () => {
|
|
480
|
+
const registry = new ProtocolRegistry();
|
|
481
|
+
const suilend = createMockLending({
|
|
482
|
+
id: 'suilend',
|
|
483
|
+
name: 'Suilend',
|
|
484
|
+
getHealth: vi.fn().mockResolvedValue({
|
|
485
|
+
healthFactor: Infinity,
|
|
486
|
+
supplied: 50,
|
|
487
|
+
borrowed: 0,
|
|
488
|
+
maxBorrow: 40,
|
|
489
|
+
liquidationThreshold: 0.8,
|
|
490
|
+
}),
|
|
491
|
+
maxWithdraw: vi.fn().mockResolvedValue({ maxAmount: 50.0, healthFactorAfter: Infinity, currentHF: Infinity }),
|
|
492
|
+
});
|
|
493
|
+
registry.registerLending(suilend);
|
|
494
|
+
const adapter = registry.getLending('suilend');
|
|
495
|
+
const hf = await adapter.getHealth('0xtest');
|
|
496
|
+
expect(hf.borrowed).toBe(0);
|
|
497
|
+
const max = await adapter.maxWithdraw('0xtest', 'USDC');
|
|
498
|
+
expect(max.maxAmount).toBe(50.0);
|
|
499
|
+
});
|
|
500
|
+
});
|
|
501
|
+
// ─── swap adapter routing ─────────────────────────────────────
|
|
502
|
+
describe('swap — multi-adapter routing', () => {
|
|
503
|
+
it('bestSwapQuote picks adapter with best output', async () => {
|
|
504
|
+
const registry = new ProtocolRegistry();
|
|
505
|
+
const { SwapAdapter: _unused, ...rest } = {};
|
|
506
|
+
const worse = {
|
|
507
|
+
id: 'worse-dex',
|
|
508
|
+
name: 'Worse DEX',
|
|
509
|
+
version: '1.0.0',
|
|
510
|
+
capabilities: ['swap'],
|
|
511
|
+
init: vi.fn(),
|
|
512
|
+
getQuote: vi.fn().mockResolvedValue({ expectedOutput: 90, priceImpact: 0.03, poolPrice: 0.95 }),
|
|
513
|
+
buildSwapTx: vi.fn(),
|
|
514
|
+
getSupportedPairs: vi.fn().mockReturnValue([{ from: 'USDC', to: 'SUI' }, { from: 'SUI', to: 'USDC' }]),
|
|
515
|
+
getPoolPrice: vi.fn().mockResolvedValue(0.95),
|
|
516
|
+
};
|
|
517
|
+
const better = {
|
|
518
|
+
id: 'better-dex',
|
|
519
|
+
name: 'Better DEX',
|
|
520
|
+
version: '1.0.0',
|
|
521
|
+
capabilities: ['swap'],
|
|
522
|
+
init: vi.fn(),
|
|
523
|
+
getQuote: vi.fn().mockResolvedValue({ expectedOutput: 105, priceImpact: 0.01, poolPrice: 1.05 }),
|
|
524
|
+
buildSwapTx: vi.fn(),
|
|
525
|
+
getSupportedPairs: vi.fn().mockReturnValue([{ from: 'USDC', to: 'SUI' }, { from: 'SUI', to: 'USDC' }]),
|
|
526
|
+
getPoolPrice: vi.fn().mockResolvedValue(1.05),
|
|
527
|
+
};
|
|
528
|
+
registry.registerSwap(worse);
|
|
529
|
+
registry.registerSwap(better);
|
|
530
|
+
const result = await registry.bestSwapQuote('USDC', 'SUI', 100);
|
|
531
|
+
expect(result.adapter.id).toBe('better-dex');
|
|
532
|
+
expect(result.quote.expectedOutput).toBe(105);
|
|
533
|
+
});
|
|
534
|
+
it('bestSwapQuote skips adapter that throws', async () => {
|
|
535
|
+
const registry = new ProtocolRegistry();
|
|
536
|
+
const broken = {
|
|
537
|
+
id: 'broken-dex',
|
|
538
|
+
name: 'Broken DEX',
|
|
539
|
+
version: '1.0.0',
|
|
540
|
+
capabilities: ['swap'],
|
|
541
|
+
init: vi.fn(),
|
|
542
|
+
getQuote: vi.fn().mockRejectedValue(new Error('pool not found')),
|
|
543
|
+
buildSwapTx: vi.fn(),
|
|
544
|
+
getSupportedPairs: vi.fn().mockReturnValue([{ from: 'USDC', to: 'SUI' }]),
|
|
545
|
+
getPoolPrice: vi.fn(),
|
|
546
|
+
};
|
|
547
|
+
const good = {
|
|
548
|
+
id: 'good-dex',
|
|
549
|
+
name: 'Good DEX',
|
|
550
|
+
version: '1.0.0',
|
|
551
|
+
capabilities: ['swap'],
|
|
552
|
+
init: vi.fn(),
|
|
553
|
+
getQuote: vi.fn().mockResolvedValue({ expectedOutput: 100, priceImpact: 0.01, poolPrice: 1.0 }),
|
|
554
|
+
buildSwapTx: vi.fn(),
|
|
555
|
+
getSupportedPairs: vi.fn().mockReturnValue([{ from: 'USDC', to: 'SUI' }]),
|
|
556
|
+
getPoolPrice: vi.fn().mockResolvedValue(1.0),
|
|
557
|
+
};
|
|
558
|
+
registry.registerSwap(broken);
|
|
559
|
+
registry.registerSwap(good);
|
|
560
|
+
const result = await registry.bestSwapQuote('USDC', 'SUI', 100);
|
|
561
|
+
expect(result.adapter.id).toBe('good-dex');
|
|
562
|
+
});
|
|
563
|
+
it('bestSwapQuote throws when no adapter supports pair', async () => {
|
|
564
|
+
const registry = new ProtocolRegistry();
|
|
565
|
+
const dex = {
|
|
566
|
+
id: 'dex',
|
|
567
|
+
name: 'DEX',
|
|
568
|
+
version: '1.0.0',
|
|
569
|
+
capabilities: ['swap'],
|
|
570
|
+
init: vi.fn(),
|
|
571
|
+
getQuote: vi.fn(),
|
|
572
|
+
buildSwapTx: vi.fn(),
|
|
573
|
+
getSupportedPairs: vi.fn().mockReturnValue([{ from: 'USDC', to: 'SUI' }]),
|
|
574
|
+
getPoolPrice: vi.fn(),
|
|
575
|
+
};
|
|
576
|
+
registry.registerSwap(dex);
|
|
577
|
+
await expect(registry.bestSwapQuote('BTC', 'ETH', 1)).rejects.toThrow('No swap adapter supports BTC → ETH');
|
|
578
|
+
});
|
|
579
|
+
});
|
|
580
|
+
// ─── rebalance: yield optimization ───────────────
|
|
581
|
+
describe('rebalance — yield optimization', () => {
|
|
582
|
+
let registry;
|
|
583
|
+
beforeEach(() => {
|
|
584
|
+
registry = new ProtocolRegistry();
|
|
585
|
+
});
|
|
586
|
+
it('identifies lowest-yield position and best opportunity', async () => {
|
|
587
|
+
const navi = createMockLending({
|
|
588
|
+
id: 'navi',
|
|
589
|
+
name: 'NAVI',
|
|
590
|
+
supportedAssets: ['USDC', 'USDT'],
|
|
591
|
+
getRates: vi.fn().mockImplementation((asset) => asset === 'USDC'
|
|
592
|
+
? { asset: 'USDC', saveApy: 3.0, borrowApy: 6.0 }
|
|
593
|
+
: { asset: 'USDT', saveApy: 5.5, borrowApy: 7.0 }),
|
|
594
|
+
getPositions: vi.fn().mockResolvedValue({
|
|
595
|
+
supplies: [{ asset: 'USDC', amount: 1000, apy: 3.0 }],
|
|
596
|
+
borrows: [],
|
|
597
|
+
}),
|
|
598
|
+
});
|
|
599
|
+
registry.registerLending(navi);
|
|
600
|
+
const allRates = await registry.allRatesAcrossAssets();
|
|
601
|
+
expect(allRates.length).toBeGreaterThanOrEqual(2);
|
|
602
|
+
const best = allRates.reduce((a, b) => b.rates.saveApy > a.rates.saveApy ? b : a);
|
|
603
|
+
expect(best.asset).toBe('USDT');
|
|
604
|
+
expect(best.rates.saveApy).toBe(5.5);
|
|
605
|
+
});
|
|
606
|
+
it('bestSaveRateAcrossAssets returns global best', async () => {
|
|
607
|
+
const low = createMockLending({
|
|
608
|
+
id: 'low',
|
|
609
|
+
name: 'Low',
|
|
610
|
+
supportedAssets: ['USDC'],
|
|
611
|
+
getRates: vi.fn().mockResolvedValue({ asset: 'USDC', saveApy: 2.0, borrowApy: 5.0 }),
|
|
612
|
+
});
|
|
613
|
+
const high = createMockLending({
|
|
614
|
+
id: 'high',
|
|
615
|
+
name: 'High',
|
|
616
|
+
supportedAssets: ['USDC', 'USDT'],
|
|
617
|
+
getRates: vi.fn().mockImplementation((asset) => asset === 'USDT'
|
|
618
|
+
? { asset: 'USDT', saveApy: 8.0, borrowApy: 10.0 }
|
|
619
|
+
: { asset: 'USDC', saveApy: 4.0, borrowApy: 6.0 }),
|
|
620
|
+
});
|
|
621
|
+
registry.registerLending(low);
|
|
622
|
+
registry.registerLending(high);
|
|
623
|
+
const result = await registry.bestSaveRateAcrossAssets();
|
|
624
|
+
expect(result.asset).toBe('USDT');
|
|
625
|
+
expect(result.rate.saveApy).toBe(8.0);
|
|
626
|
+
expect(result.adapter.name).toBe('High');
|
|
627
|
+
});
|
|
628
|
+
it('calculates break-even days for cross-asset moves', () => {
|
|
629
|
+
const swapCost = 0.30;
|
|
630
|
+
const annualGain = 11.90;
|
|
631
|
+
const breakEvenDays = Math.ceil((swapCost / annualGain) * 365);
|
|
632
|
+
expect(breakEvenDays).toBe(10);
|
|
633
|
+
});
|
|
634
|
+
it('same-asset rebalance has zero swap cost', () => {
|
|
635
|
+
const isSameAsset = true;
|
|
636
|
+
const estimatedSwapCost = isSameAsset ? 0 : 0.30;
|
|
637
|
+
const breakEvenDays = estimatedSwapCost > 0 ? Math.ceil((estimatedSwapCost / 6.9) * 365) : 0;
|
|
638
|
+
expect(estimatedSwapCost).toBe(0);
|
|
639
|
+
expect(breakEvenDays).toBe(0);
|
|
640
|
+
});
|
|
641
|
+
it('skips opportunities below minYieldDiff', () => {
|
|
642
|
+
const currentApy = 4.2;
|
|
643
|
+
const bestApy = 4.5;
|
|
644
|
+
const minYieldDiff = 0.5;
|
|
645
|
+
const apyDiff = bestApy - currentApy;
|
|
646
|
+
expect(apyDiff < minYieldDiff).toBe(true);
|
|
647
|
+
});
|
|
648
|
+
it('skips cross-asset moves with break-even > maxBreakEven', () => {
|
|
649
|
+
const swapCost = 5.0;
|
|
650
|
+
const annualGain = 2.0;
|
|
651
|
+
const maxBreakEven = 30;
|
|
652
|
+
const breakEvenDays = Math.ceil((swapCost / annualGain) * 365);
|
|
653
|
+
expect(breakEvenDays).toBeGreaterThan(maxBreakEven);
|
|
654
|
+
});
|
|
655
|
+
it('handles empty positions (nothing to rebalance)', async () => {
|
|
656
|
+
const empty = createMockLending({
|
|
657
|
+
id: 'empty',
|
|
658
|
+
name: 'Empty',
|
|
659
|
+
getPositions: vi.fn().mockResolvedValue({ supplies: [], borrows: [] }),
|
|
660
|
+
});
|
|
661
|
+
registry.registerLending(empty);
|
|
662
|
+
const positions = await registry.allPositions('0x123');
|
|
663
|
+
const savePositions = positions.flatMap(p => p.positions.supplies.filter(s => s.amount > 0.01));
|
|
664
|
+
expect(savePositions.length).toBe(0);
|
|
665
|
+
});
|
|
666
|
+
it('health factor check prevents unsafe rebalance', () => {
|
|
667
|
+
const healthFactor = 1.3;
|
|
668
|
+
const threshold = 1.5;
|
|
669
|
+
expect(healthFactor < threshold).toBe(true);
|
|
670
|
+
});
|
|
671
|
+
});
|
|
672
|
+
// ─── borrow multi-stable ───────────────
|
|
673
|
+
describe('borrow multi-stable', () => {
|
|
674
|
+
let registry;
|
|
675
|
+
beforeEach(() => {
|
|
676
|
+
registry = new ProtocolRegistry();
|
|
677
|
+
});
|
|
678
|
+
it('finds adapter supporting USDT borrow', () => {
|
|
679
|
+
const navi = createMockLending({
|
|
680
|
+
id: 'navi',
|
|
681
|
+
name: 'NAVI',
|
|
682
|
+
supportedAssets: ['USDC', 'USDT', 'USDe', 'USDsui'],
|
|
683
|
+
capabilities: ['save', 'withdraw', 'borrow', 'repay'],
|
|
684
|
+
});
|
|
685
|
+
registry.registerLending(navi);
|
|
686
|
+
const adapters = registry.listLending().filter(a => a.supportedAssets.includes('USDT') && a.capabilities.includes('borrow'));
|
|
687
|
+
expect(adapters.length).toBe(1);
|
|
688
|
+
expect(adapters[0].id).toBe('navi');
|
|
689
|
+
});
|
|
690
|
+
it('finds adapter supporting USDe borrow', () => {
|
|
691
|
+
const navi = createMockLending({
|
|
692
|
+
id: 'navi',
|
|
693
|
+
name: 'NAVI',
|
|
694
|
+
supportedAssets: ['USDC', 'USDT', 'USDe', 'USDsui'],
|
|
695
|
+
});
|
|
696
|
+
registry.registerLending(navi);
|
|
697
|
+
const adapters = registry.listLending().filter(a => a.supportedAssets.includes('USDe') && a.capabilities.includes('borrow'));
|
|
698
|
+
expect(adapters.length).toBe(1);
|
|
699
|
+
});
|
|
700
|
+
it('defaults to USDC when no asset specified', () => {
|
|
701
|
+
const params = {};
|
|
702
|
+
const asset = params.asset ?? 'USDC';
|
|
703
|
+
expect(asset).toBe('USDC');
|
|
704
|
+
});
|
|
705
|
+
it('returns error with alternatives when asset not supported', () => {
|
|
706
|
+
const suilend = createMockLending({
|
|
707
|
+
id: 'suilend',
|
|
708
|
+
name: 'Suilend',
|
|
709
|
+
supportedAssets: ['USDC'],
|
|
710
|
+
capabilities: ['save', 'withdraw'],
|
|
711
|
+
supportsSameAssetBorrow: false,
|
|
712
|
+
});
|
|
713
|
+
registry.registerLending(suilend);
|
|
714
|
+
const adapters = registry.listLending().filter(a => a.supportedAssets.includes('WBTC') && a.capabilities.includes('borrow'));
|
|
715
|
+
expect(adapters.length).toBe(0);
|
|
716
|
+
const alternatives = registry.listLending().filter(a => a.capabilities.includes('borrow') || a.capabilities.includes('save'));
|
|
717
|
+
expect(alternatives.length).toBeGreaterThan(0);
|
|
718
|
+
});
|
|
719
|
+
it('repay resolves correct asset adapter', () => {
|
|
720
|
+
const navi = createMockLending({
|
|
721
|
+
id: 'navi',
|
|
722
|
+
name: 'NAVI',
|
|
723
|
+
supportedAssets: ['USDC', 'USDT', 'USDe', 'USDsui'],
|
|
724
|
+
});
|
|
725
|
+
registry.registerLending(navi);
|
|
726
|
+
const asset = 'USDT';
|
|
727
|
+
const adapters = registry.listLending().filter(a => a.supportedAssets.includes(asset) && a.capabilities.includes('repay'));
|
|
728
|
+
expect(adapters.length).toBe(1);
|
|
729
|
+
expect(adapters[0].id).toBe('navi');
|
|
730
|
+
});
|
|
731
|
+
});
|
|
732
|
+
// ─── withdraw auto-swap-to-USDC ────────────────────────────────
|
|
733
|
+
describe('withdraw — auto-swap non-USDC to USDC', () => {
|
|
734
|
+
function createMockSwap(overrides = {}) {
|
|
735
|
+
return {
|
|
736
|
+
id: 'mock-dex',
|
|
737
|
+
name: 'Mock DEX',
|
|
738
|
+
version: '1.0.0',
|
|
739
|
+
capabilities: ['swap'],
|
|
740
|
+
init: vi.fn(),
|
|
741
|
+
getQuote: vi.fn().mockResolvedValue({ expectedOutput: 10, priceImpact: 0.001, poolPrice: 1.0 }),
|
|
742
|
+
buildSwapTx: vi.fn().mockResolvedValue({
|
|
743
|
+
tx: new Transaction(),
|
|
744
|
+
estimatedOut: 10_000_000,
|
|
745
|
+
toDecimals: 6,
|
|
746
|
+
}),
|
|
747
|
+
getSupportedPairs: vi.fn().mockReturnValue([
|
|
748
|
+
{ from: 'USDT', to: 'USDC' },
|
|
749
|
+
{ from: 'USDe', to: 'USDC' },
|
|
750
|
+
{ from: 'USDsui', to: 'USDC' },
|
|
751
|
+
]),
|
|
752
|
+
getPoolPrice: vi.fn().mockResolvedValue(1.0),
|
|
753
|
+
...overrides,
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
it('identifies non-USDC position that needs swap after withdrawal', async () => {
|
|
757
|
+
const registry = new ProtocolRegistry();
|
|
758
|
+
const navi = createMockLending({
|
|
759
|
+
id: 'navi',
|
|
760
|
+
name: 'NAVI Protocol',
|
|
761
|
+
supportedAssets: ['USDC', 'USDT'],
|
|
762
|
+
getPositions: vi.fn().mockResolvedValue({
|
|
763
|
+
supplies: [{ asset: 'USDT', amount: 10.0, apy: 5.5 }],
|
|
764
|
+
borrows: [],
|
|
765
|
+
}),
|
|
766
|
+
maxWithdraw: vi.fn().mockResolvedValue({ maxAmount: 10.0, healthFactorAfter: 999, currentHF: 999 }),
|
|
767
|
+
buildWithdrawTx: vi.fn().mockResolvedValue({ tx: new Transaction(), effectiveAmount: 10.0 }),
|
|
768
|
+
});
|
|
769
|
+
registry.registerLending(navi);
|
|
770
|
+
const positions = await registry.allPositions('0xtest');
|
|
771
|
+
const supplies = positions.flatMap(p => p.positions.supplies.filter(s => s.amount > 0.001).map(s => ({
|
|
772
|
+
protocolId: p.protocolId,
|
|
773
|
+
asset: s.asset,
|
|
774
|
+
amount: s.amount,
|
|
775
|
+
})));
|
|
776
|
+
expect(supplies).toHaveLength(1);
|
|
777
|
+
expect(supplies[0].asset).toBe('USDT');
|
|
778
|
+
expect(supplies[0].asset).not.toBe('USDC');
|
|
779
|
+
});
|
|
780
|
+
it('swap adapter is called with correct from/to for USDT→USDC', async () => {
|
|
781
|
+
const registry = new ProtocolRegistry();
|
|
782
|
+
const swapAdapter = createMockSwap();
|
|
783
|
+
registry.registerSwap(swapAdapter);
|
|
784
|
+
const swapAdapters = registry.listSwap();
|
|
785
|
+
expect(swapAdapters).toHaveLength(1);
|
|
786
|
+
const from = 'USDT';
|
|
787
|
+
const to = 'USDC';
|
|
788
|
+
const amount = 10.0;
|
|
789
|
+
await swapAdapters[0].buildSwapTx('0xtest', from, to, amount);
|
|
790
|
+
expect(swapAdapter.buildSwapTx).toHaveBeenCalledWith('0xtest', 'USDT', 'USDC', 10.0);
|
|
791
|
+
});
|
|
792
|
+
it('swap adapter handles all stable-to-USDC pairs', async () => {
|
|
793
|
+
const registry = new ProtocolRegistry();
|
|
794
|
+
const swapAdapter = createMockSwap();
|
|
795
|
+
registry.registerSwap(swapAdapter);
|
|
796
|
+
const pairs = swapAdapter.getSupportedPairs();
|
|
797
|
+
const toUsdcPairs = pairs.filter(p => p.to === 'USDC');
|
|
798
|
+
expect(toUsdcPairs.length).toBeGreaterThanOrEqual(3);
|
|
799
|
+
expect(toUsdcPairs.map(p => p.from).sort()).toEqual(['USDT', 'USDe', 'USDsui']);
|
|
800
|
+
});
|
|
801
|
+
it('withdraw all collects positions from multiple protocols with mixed assets', async () => {
|
|
802
|
+
const registry = new ProtocolRegistry();
|
|
803
|
+
const navi = createMockLending({
|
|
804
|
+
id: 'navi',
|
|
805
|
+
name: 'NAVI Protocol',
|
|
806
|
+
supportedAssets: ['USDC', 'USDT'],
|
|
807
|
+
getPositions: vi.fn().mockResolvedValue({
|
|
808
|
+
supplies: [{ asset: 'USDT', amount: 10.0, apy: 5.5 }],
|
|
809
|
+
borrows: [],
|
|
810
|
+
}),
|
|
811
|
+
maxWithdraw: vi.fn().mockResolvedValue({ maxAmount: 10.0, healthFactorAfter: 999, currentHF: 999 }),
|
|
812
|
+
});
|
|
813
|
+
const suilend = createMockLending({
|
|
814
|
+
id: 'suilend',
|
|
815
|
+
name: 'Suilend',
|
|
816
|
+
getPositions: vi.fn().mockResolvedValue({
|
|
817
|
+
supplies: [{ asset: 'USDC', amount: 5.0, apy: 2.2 }],
|
|
818
|
+
borrows: [],
|
|
819
|
+
}),
|
|
820
|
+
maxWithdraw: vi.fn().mockResolvedValue({ maxAmount: 5.0, healthFactorAfter: 999, currentHF: 999 }),
|
|
821
|
+
});
|
|
822
|
+
registry.registerLending(navi);
|
|
823
|
+
registry.registerLending(suilend);
|
|
824
|
+
const positions = await registry.allPositions('0xtest');
|
|
825
|
+
const withdrawable = positions.flatMap(p => p.positions.supplies
|
|
826
|
+
.filter(s => s.amount > 0.001)
|
|
827
|
+
.map(s => ({ protocolId: p.protocolId, asset: s.asset, amount: s.amount })));
|
|
828
|
+
expect(withdrawable).toHaveLength(2);
|
|
829
|
+
const usdt = withdrawable.find(w => w.asset === 'USDT');
|
|
830
|
+
const usdc = withdrawable.find(w => w.asset === 'USDC');
|
|
831
|
+
expect(usdt).toBeDefined();
|
|
832
|
+
expect(usdt.protocolId).toBe('navi');
|
|
833
|
+
expect(usdc).toBeDefined();
|
|
834
|
+
expect(usdc.protocolId).toBe('suilend');
|
|
835
|
+
// USDT needs swap, USDC does not
|
|
836
|
+
expect(usdt.asset !== 'USDC').toBe(true);
|
|
837
|
+
expect(usdc.asset === 'USDC').toBe(true);
|
|
838
|
+
});
|
|
839
|
+
it('sorts positions by APY ascending for withdraw (worst yield first)', () => {
|
|
840
|
+
const supplies = [
|
|
841
|
+
{ protocolId: 'navi', asset: 'USDT', amount: 10, apy: 5.5 },
|
|
842
|
+
{ protocolId: 'suilend', asset: 'USDC', amount: 5, apy: 2.2 },
|
|
843
|
+
{ protocolId: 'navi', asset: 'USDC', amount: 8, apy: 4.9 },
|
|
844
|
+
];
|
|
845
|
+
supplies.sort((a, b) => a.apy - b.apy);
|
|
846
|
+
expect(supplies[0].apy).toBe(2.2);
|
|
847
|
+
expect(supplies[0].protocolId).toBe('suilend');
|
|
848
|
+
});
|
|
849
|
+
});
|
|
850
|
+
// ─── claim rewards: multi-protocol orchestration ──────────────
|
|
851
|
+
describe('claim rewards — orchestration', () => {
|
|
852
|
+
it('aggregates pending rewards across multiple adapters', async () => {
|
|
853
|
+
const navi = createMockLending({
|
|
854
|
+
id: 'navi',
|
|
855
|
+
name: 'NAVI Protocol',
|
|
856
|
+
getPendingRewards: vi.fn().mockResolvedValue([
|
|
857
|
+
{ protocol: 'navi', asset: 'USDC', coinType: '0x549::cert::CERT', symbol: 'vSUI', amount: 0, estimatedValueUsd: 0 },
|
|
858
|
+
{ protocol: 'navi', asset: 'ETH', coinType: '0x549::cert::CERT', symbol: 'vSUI', amount: 0, estimatedValueUsd: 0 },
|
|
859
|
+
]),
|
|
860
|
+
});
|
|
861
|
+
const suilend = createMockLending({
|
|
862
|
+
id: 'suilend',
|
|
863
|
+
name: 'Suilend',
|
|
864
|
+
getPendingRewards: vi.fn().mockResolvedValue([
|
|
865
|
+
{ protocol: 'suilend', asset: 'SUI', coinType: '0x835::spring_sui::SPRING_SUI', symbol: 'sSUI', amount: 0, estimatedValueUsd: 0 },
|
|
866
|
+
]),
|
|
867
|
+
});
|
|
868
|
+
const registry = new ProtocolRegistry();
|
|
869
|
+
registry.registerLending(navi);
|
|
870
|
+
registry.registerLending(suilend);
|
|
871
|
+
const allRewards = [];
|
|
872
|
+
for (const adapter of registry.listLending()) {
|
|
873
|
+
if (adapter.getPendingRewards) {
|
|
874
|
+
const rewards = await adapter.getPendingRewards('0xaddr');
|
|
875
|
+
allRewards.push(...rewards);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
expect(allRewards).toHaveLength(3);
|
|
879
|
+
expect(allRewards.filter(r => r.protocol === 'navi')).toHaveLength(2);
|
|
880
|
+
expect(allRewards.filter(r => r.protocol === 'suilend')).toHaveLength(1);
|
|
881
|
+
});
|
|
882
|
+
it('skips adapters without getPendingRewards', async () => {
|
|
883
|
+
const navi = createMockLending({
|
|
884
|
+
id: 'navi',
|
|
885
|
+
name: 'NAVI Protocol',
|
|
886
|
+
getPendingRewards: vi.fn().mockResolvedValue([
|
|
887
|
+
{ protocol: 'navi', asset: 'USDC', coinType: '0x549::cert::CERT', symbol: 'vSUI', amount: 0, estimatedValueUsd: 0 },
|
|
888
|
+
]),
|
|
889
|
+
});
|
|
890
|
+
const legacyAdapter = createMockLending({
|
|
891
|
+
id: 'legacy',
|
|
892
|
+
name: 'Legacy Protocol',
|
|
893
|
+
});
|
|
894
|
+
const registry = new ProtocolRegistry();
|
|
895
|
+
registry.registerLending(navi);
|
|
896
|
+
registry.registerLending(legacyAdapter);
|
|
897
|
+
const allRewards = [];
|
|
898
|
+
for (const adapter of registry.listLending()) {
|
|
899
|
+
if (adapter.getPendingRewards) {
|
|
900
|
+
const rewards = await adapter.getPendingRewards('0xaddr');
|
|
901
|
+
allRewards.push(...rewards);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
expect(allRewards).toHaveLength(1);
|
|
905
|
+
expect(allRewards[0].protocol).toBe('navi');
|
|
906
|
+
});
|
|
907
|
+
it('deduplicates reward tokens for swap', () => {
|
|
908
|
+
const rewards = [
|
|
909
|
+
{ protocol: 'navi', asset: 'USDC', coinType: '0x549::cert::CERT', symbol: 'vSUI', amount: 0, estimatedValueUsd: 0 },
|
|
910
|
+
{ protocol: 'navi', asset: 'ETH', coinType: '0x549::cert::CERT', symbol: 'vSUI', amount: 0, estimatedValueUsd: 0 },
|
|
911
|
+
{ protocol: 'navi', asset: 'GOLD', coinType: '0xdeeb::deep::DEEP', symbol: 'DEEP', amount: 0, estimatedValueUsd: 0 },
|
|
912
|
+
];
|
|
913
|
+
const uniqueTokens = [...new Set(rewards.map(r => r.coinType))];
|
|
914
|
+
expect(uniqueTokens).toHaveLength(2);
|
|
915
|
+
expect(uniqueTokens).toContain('0x549::cert::CERT');
|
|
916
|
+
expect(uniqueTokens).toContain('0xdeeb::deep::DEEP');
|
|
917
|
+
});
|
|
918
|
+
it('builds correct reward-by-asset mapping for positions display', () => {
|
|
919
|
+
const rewards = [
|
|
920
|
+
{ protocol: 'navi', asset: 'USDC', coinType: '0x549::cert::CERT', symbol: 'vSUI', amount: 0, estimatedValueUsd: 0 },
|
|
921
|
+
{ protocol: 'navi', asset: 'ETH', coinType: '0x549::cert::CERT', symbol: 'vSUI', amount: 0, estimatedValueUsd: 0 },
|
|
922
|
+
{ protocol: 'suilend', asset: 'SUI', coinType: '0x835::spring_sui::SPRING_SUI', symbol: 'sSUI', amount: 0, estimatedValueUsd: 0 },
|
|
923
|
+
];
|
|
924
|
+
const rewardKeys = new Set(rewards.map(r => `${r.protocol}:${r.asset}`));
|
|
925
|
+
expect(rewardKeys.has('navi:USDC')).toBe(true);
|
|
926
|
+
expect(rewardKeys.has('navi:ETH')).toBe(true);
|
|
927
|
+
expect(rewardKeys.has('suilend:SUI')).toBe(true);
|
|
928
|
+
expect(rewardKeys.has('navi:GOLD')).toBe(false);
|
|
929
|
+
});
|
|
930
|
+
});
|
|
931
|
+
describe('SUPPORTED_ASSETS', () => {
|
|
932
|
+
it('includes all 4 stables and SUI', async () => {
|
|
933
|
+
const { SUPPORTED_ASSETS, STABLE_ASSETS } = await import('./constants.js');
|
|
934
|
+
expect(SUPPORTED_ASSETS.USDC).toBeDefined();
|
|
935
|
+
expect(SUPPORTED_ASSETS.USDT).toBeDefined();
|
|
936
|
+
expect(SUPPORTED_ASSETS.USDe).toBeDefined();
|
|
937
|
+
expect(SUPPORTED_ASSETS.USDsui).toBeDefined();
|
|
938
|
+
expect(SUPPORTED_ASSETS.SUI).toBeDefined();
|
|
939
|
+
expect(STABLE_ASSETS).toEqual(['USDC', 'USDT', 'USDe', 'USDsui']);
|
|
940
|
+
});
|
|
941
|
+
it('all stables have 6 decimals', async () => {
|
|
942
|
+
const { SUPPORTED_ASSETS, STABLE_ASSETS } = await import('./constants.js');
|
|
943
|
+
for (const asset of STABLE_ASSETS) {
|
|
944
|
+
expect(SUPPORTED_ASSETS[asset].decimals).toBe(6);
|
|
945
|
+
}
|
|
946
|
+
});
|
|
947
|
+
it('all assets have displayName', async () => {
|
|
948
|
+
const { SUPPORTED_ASSETS } = await import('./constants.js');
|
|
949
|
+
for (const [, info] of Object.entries(SUPPORTED_ASSETS)) {
|
|
950
|
+
expect(info.displayName).toBeTruthy();
|
|
951
|
+
}
|
|
952
|
+
});
|
|
953
|
+
});
|
|
954
|
+
//# sourceMappingURL=t2000.integration.test.js.map
|