@kasufinance/kasu-sdk 2.6.0 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +128 -3
- package/dist/bundle.cjs.js +1119 -254
- package/dist/bundle.esm.js +1109 -255
- package/dist/domain/index.d.ts +3 -2
- package/dist/domain/index.js +2 -2
- package/dist/domain/index.js.map +1 -1
- package/dist/domain/requests.d.ts +42 -4
- package/dist/domain/requests.js +20 -4
- package/dist/domain/requests.js.map +1 -1
- package/dist/domain/wallet-errors.d.ts +63 -8
- package/dist/domain/wallet-errors.js +115 -16
- package/dist/domain/wallet-errors.js.map +1 -1
- package/dist/facade/deposits.js +2 -1
- package/dist/facade/deposits.js.map +1 -1
- package/dist/facade/flows.d.ts +95 -0
- package/dist/facade/flows.js +116 -0
- package/dist/facade/flows.js.map +1 -0
- package/dist/facade/index.d.ts +3 -0
- package/dist/facade/index.js +3 -0
- package/dist/facade/index.js.map +1 -1
- package/dist/facade/kasu.d.ts +6 -2
- package/dist/facade/kasu.js +28 -3
- package/dist/facade/kasu.js.map +1 -1
- package/dist/facade/read-only.d.ts +12 -0
- package/dist/facade/read-only.js +13 -0
- package/dist/facade/read-only.js.map +1 -0
- package/dist/flows/deposit-flow.d.ts +301 -0
- package/dist/flows/deposit-flow.js +358 -0
- package/dist/flows/deposit-flow.js.map +1 -0
- package/dist/flows/flow.d.ts +72 -0
- package/dist/flows/flow.js +110 -0
- package/dist/flows/flow.js.map +1 -0
- package/dist/flows/index.d.ts +19 -0
- package/dist/flows/index.js +17 -0
- package/dist/flows/index.js.map +1 -0
- package/dist/flows/observable.d.ts +59 -0
- package/dist/flows/observable.js +81 -0
- package/dist/flows/observable.js.map +1 -0
- package/dist/flows/withdraw-flow.d.ts +99 -0
- package/dist/flows/withdraw-flow.js +83 -0
- package/dist/flows/withdraw-flow.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/services/DataService/data-service.js +0 -1
- package/dist/services/DataService/data-service.js.map +1 -1
- package/package.json +8 -2
- package/src/domain/index.ts +7 -1
- package/src/domain/requests.ts +57 -4
- package/src/domain/wallet-errors.ts +135 -18
- package/src/facade/deposits.ts +2 -3
- package/src/facade/flows.ts +172 -0
- package/src/facade/index.ts +8 -0
- package/src/facade/kasu.ts +43 -3
- package/src/facade/read-only.ts +13 -0
- package/src/flows/deposit-flow.ts +775 -0
- package/src/flows/flow.ts +108 -0
- package/src/flows/index.ts +45 -0
- package/src/flows/observable.ts +97 -0
- package/src/flows/withdraw-flow.ts +210 -0
- package/src/index.ts +7 -0
- package/src/services/DataService/data-service.ts +0 -1
- package/dist/domain/au-minimum.test.d.ts +0 -1
- package/dist/domain/au-minimum.test.js +0 -202
- package/dist/domain/au-minimum.test.js.map +0 -1
- package/dist/domain/deposit-bounds.test.d.ts +0 -1
- package/dist/domain/deposit-bounds.test.js +0 -135
- package/dist/domain/deposit-bounds.test.js.map +0 -1
- package/dist/domain/loan-contract.test.d.ts +0 -1
- package/dist/domain/loan-contract.test.js +0 -255
- package/dist/domain/loan-contract.test.js.map +0 -1
- package/dist/domain/partners.test.d.ts +0 -1
- package/dist/domain/partners.test.js +0 -53
- package/dist/domain/partners.test.js.map +0 -1
- package/dist/domain/pools.test.d.ts +0 -1
- package/dist/domain/pools.test.js +0 -184
- package/dist/domain/pools.test.js.map +0 -1
- package/dist/domain/rates.test.d.ts +0 -1
- package/dist/domain/rates.test.js +0 -181
- package/dist/domain/rates.test.js.map +0 -1
- package/dist/domain/requests.test.d.ts +0 -1
- package/dist/domain/requests.test.js +0 -470
- package/dist/domain/requests.test.js.map +0 -1
- package/dist/domain/settlement.test.d.ts +0 -1
- package/dist/domain/settlement.test.js +0 -152
- package/dist/domain/settlement.test.js.map +0 -1
- package/dist/domain/tranche-display-name.test.d.ts +0 -1
- package/dist/domain/tranche-display-name.test.js +0 -58
- package/dist/domain/tranche-display-name.test.js.map +0 -1
- package/dist/domain/tranches.test.d.ts +0 -1
- package/dist/domain/tranches.test.js +0 -206
- package/dist/domain/tranches.test.js.map +0 -1
- package/dist/domain/wallet-errors.test.d.ts +0 -1
- package/dist/domain/wallet-errors.test.js +0 -71
- package/dist/domain/wallet-errors.test.js.map +0 -1
- package/dist/facade/config.test.d.ts +0 -1
- package/dist/facade/config.test.js +0 -216
- package/dist/facade/config.test.js.map +0 -1
- package/dist/facade/facade.test.d.ts +0 -1
- package/dist/facade/facade.test.js +0 -278
- package/dist/facade/facade.test.js.map +0 -1
- package/dist/services/Locking/calculate-apy.test.d.ts +0 -1
- package/dist/services/Locking/calculate-apy.test.js +0 -41
- package/dist/services/Locking/calculate-apy.test.js.map +0 -1
- package/dist/tests/sample.test.d.ts +0 -1
- package/dist/tests/sample.test.js +0 -59
- package/dist/tests/sample.test.js.map +0 -1
- package/src/domain/au-minimum.test.ts +0 -371
- package/src/domain/deposit-bounds.test.ts +0 -200
- package/src/domain/loan-contract.test.ts +0 -343
- package/src/domain/partners.test.ts +0 -83
- package/src/domain/pools.test.ts +0 -260
- package/src/domain/rates.test.ts +0 -254
- package/src/domain/requests.test.ts +0 -653
- package/src/domain/settlement.test.ts +0 -198
- package/src/domain/tranche-display-name.test.ts +0 -96
- package/src/domain/tranches.test.ts +0 -300
- package/src/domain/wallet-errors.test.ts +0 -100
- package/src/facade/config.test.ts +0 -265
- package/src/facade/facade.test.ts +0 -374
- package/src/services/Locking/calculate-apy.test.ts +0 -52
- package/src/tests/sample.test.ts +0 -63
|
@@ -1,265 +0,0 @@
|
|
|
1
|
-
import { providers, Wallet } from 'ethers';
|
|
2
|
-
|
|
3
|
-
import { SdkConfig } from '../sdk-config';
|
|
4
|
-
|
|
5
|
-
import { CHAIN_CONFIGS } from './chain-configs';
|
|
6
|
-
import { Kasu } from './kasu';
|
|
7
|
-
import { ChainConfigEntry } from './types';
|
|
8
|
-
|
|
9
|
-
// Pure config assertions — no network, unlike `facade.test.ts`.
|
|
10
|
-
|
|
11
|
-
const BASE_CONTRACTS = CHAIN_CONFIGS.base.contracts;
|
|
12
|
-
|
|
13
|
-
/** A signer with a provider attached, offline — nothing here sends anything. */
|
|
14
|
-
function signer(): Wallet {
|
|
15
|
-
return Wallet.createRandom().connect(
|
|
16
|
-
new providers.StaticJsonRpcProvider(
|
|
17
|
-
CHAIN_CONFIGS.base.rpcUrls[0],
|
|
18
|
-
CHAIN_CONFIGS.base.chainId,
|
|
19
|
-
),
|
|
20
|
-
);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
describe('SdkConfig — UNUSED_LENDING_POOL_IDS normalisation', () => {
|
|
24
|
-
it("turns an empty exclusion list into the [''] sentinel", () => {
|
|
25
|
-
// `id_not_in: []` matches NOTHING in the subgraph: on Base it returns
|
|
26
|
-
// 0 pools where `['']` returns all 9 (verified live 2026-09-04).
|
|
27
|
-
const config = new SdkConfig({
|
|
28
|
-
subgraphUrl: 'https://example.invalid/subgraph',
|
|
29
|
-
contracts: BASE_CONTRACTS,
|
|
30
|
-
UNUSED_LENDING_POOL_IDS: [],
|
|
31
|
-
});
|
|
32
|
-
expect(config.UNUSED_LENDING_POOL_IDS).toEqual(['']);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it('leaves a caller-supplied exclusion list untouched', () => {
|
|
36
|
-
const config = new SdkConfig({
|
|
37
|
-
subgraphUrl: 'https://example.invalid/subgraph',
|
|
38
|
-
contracts: BASE_CONTRACTS,
|
|
39
|
-
UNUSED_LENDING_POOL_IDS: ['0xdead'],
|
|
40
|
-
});
|
|
41
|
-
expect(config.UNUSED_LENDING_POOL_IDS).toEqual(['0xdead']);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('leaves a sentinel a consumer already passes untouched', () => {
|
|
45
|
-
// kasu-ui passes [''], kasu-mobile passes the zero address.
|
|
46
|
-
const zeroAddress = '0x0000000000000000000000000000000000000000';
|
|
47
|
-
for (const sentinel of [[''], [zeroAddress]]) {
|
|
48
|
-
const config = new SdkConfig({
|
|
49
|
-
subgraphUrl: 'https://example.invalid/subgraph',
|
|
50
|
-
contracts: BASE_CONTRACTS,
|
|
51
|
-
UNUSED_LENDING_POOL_IDS: sentinel,
|
|
52
|
-
});
|
|
53
|
-
expect(config.UNUSED_LENDING_POOL_IDS).toEqual(sentinel);
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
describe('CHAIN_CONFIGS — stable asset', () => {
|
|
59
|
-
it('gives every chain exactly one stable asset', () => {
|
|
60
|
-
for (const [chain, config] of Object.entries(CHAIN_CONFIGS)) {
|
|
61
|
-
expect(config.stableAsset.address).toMatch(/^0x[0-9a-fA-F]{40}$/);
|
|
62
|
-
expect(config.stableAsset.symbol.length).toBeGreaterThan(0);
|
|
63
|
-
expect(config.stableAsset.decimals).toBeGreaterThan(0);
|
|
64
|
-
expect(config.stableAsset.currencyCode).toMatch(/^[A-Z]{3}$/);
|
|
65
|
-
expect(chain).toBeTruthy();
|
|
66
|
-
}
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it('carries the live token per deployment', () => {
|
|
70
|
-
expect(CHAIN_CONFIGS.base.stableAsset).toEqual({
|
|
71
|
-
address: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
|
|
72
|
-
symbol: 'USDC',
|
|
73
|
-
name: 'USD Coin',
|
|
74
|
-
decimals: 6,
|
|
75
|
-
currencyCode: 'USD',
|
|
76
|
-
});
|
|
77
|
-
expect(CHAIN_CONFIGS.xdc.stableAsset.symbol).toBe('AUDD');
|
|
78
|
-
expect(CHAIN_CONFIGS.xdc.stableAsset.currencyCode).toBe('AUD');
|
|
79
|
-
expect(CHAIN_CONFIGS['xdc-usdc'].stableAsset.symbol).toBe('USDC');
|
|
80
|
-
expect(CHAIN_CONFIGS.plume.stableAsset.symbol).toBe('pUSD');
|
|
81
|
-
});
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
describe('CHAIN_CONFIGS — rpcUrls', () => {
|
|
85
|
-
it('gives every live deployment at least one default endpoint', () => {
|
|
86
|
-
for (const chain of ['base', 'xdc', 'xdc-usdc'] as const) {
|
|
87
|
-
expect(CHAIN_CONFIGS[chain].rpcUrls.length).toBeGreaterThan(0);
|
|
88
|
-
expect(CHAIN_CONFIGS[chain].retired).toBeUndefined();
|
|
89
|
-
}
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it('never lists an XDC endpoint that fails CORS preflight', () => {
|
|
93
|
-
// `rpc.xdc.org` / `erpc.xdc.org` answer OPTIONS without
|
|
94
|
-
// `access-control-allow-origin`, so a browser call hangs. House rule.
|
|
95
|
-
for (const config of Object.values(CHAIN_CONFIGS)) {
|
|
96
|
-
for (const url of config.rpcUrls) {
|
|
97
|
-
expect(url).not.toContain('rpc.xdc.org');
|
|
98
|
-
expect(url).not.toContain('erpc.xdc.org');
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it('marks the retired deployment and leaves it without a default RPC', () => {
|
|
104
|
-
expect(CHAIN_CONFIGS.plume.retired).toBe(true);
|
|
105
|
-
expect(CHAIN_CONFIGS.plume.rpcUrls).toEqual([]);
|
|
106
|
-
});
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
describe('Kasu — read-only create and connect', () => {
|
|
110
|
-
it('creates a read-only instance with no signerOrProvider', () => {
|
|
111
|
-
const kasu = Kasu.create({ chain: 'base' });
|
|
112
|
-
expect(kasu.isReadOnly).toBe(true);
|
|
113
|
-
expect(kasu.provider).toBeInstanceOf(providers.StaticJsonRpcProvider);
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it('uses rpcUrls[0] and the config chain id, with no network detection', () => {
|
|
117
|
-
const kasu = Kasu.create({ chain: 'base' });
|
|
118
|
-
const provider = kasu.provider as providers.StaticJsonRpcProvider;
|
|
119
|
-
expect(provider.connection.url).toBe(CHAIN_CONFIGS.base.rpcUrls[0]);
|
|
120
|
-
expect(provider.network.chainId).toBe(CHAIN_CONFIGS.base.chainId);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it('refuses a read-only create on a retired chain with no default RPC', () => {
|
|
124
|
-
expect(() => Kasu.create({ chain: 'plume' })).toThrow(
|
|
125
|
-
'Kasu.create: chain "plume" has no default RPC (retired); pass signerOrProvider',
|
|
126
|
-
);
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it('still accepts an explicit provider for the retired chain', () => {
|
|
130
|
-
const provider = new providers.StaticJsonRpcProvider(
|
|
131
|
-
'https://example.invalid/plume',
|
|
132
|
-
CHAIN_CONFIGS.plume.chainId,
|
|
133
|
-
);
|
|
134
|
-
const kasu = Kasu.create({ chain: 'plume', signerOrProvider: provider });
|
|
135
|
-
expect(kasu.isReadOnly).toBe(true);
|
|
136
|
-
expect(kasu.provider).toBe(provider);
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it('is writable when created from a signer', () => {
|
|
140
|
-
const kasu = Kasu.create({ chain: 'base', signerOrProvider: signer() });
|
|
141
|
-
expect(kasu.isReadOnly).toBe(false);
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
it('connect returns a NEW writable instance, leaving the original read-only', () => {
|
|
145
|
-
const readOnly = Kasu.create({ chain: 'base' });
|
|
146
|
-
const connected = readOnly.connect(signer());
|
|
147
|
-
expect(connected).not.toBe(readOnly);
|
|
148
|
-
expect(connected.isReadOnly).toBe(false);
|
|
149
|
-
expect(readOnly.isReadOnly).toBe(true);
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it('connect keeps the chain config and the configOverrides', () => {
|
|
153
|
-
const readOnly = Kasu.create({
|
|
154
|
-
chain: 'base',
|
|
155
|
-
configOverrides: { UNUSED_LENDING_POOL_IDS: ['0xhidden'] },
|
|
156
|
-
});
|
|
157
|
-
const connected = readOnly.connect(signer());
|
|
158
|
-
expect(connected.chainConfig).toBe(readOnly.chainConfig);
|
|
159
|
-
const configOf = (kasu: Kasu): SdkConfig =>
|
|
160
|
-
(kasu.services.DataService as unknown as { _kasuConfig: SdkConfig })
|
|
161
|
-
._kasuConfig;
|
|
162
|
-
expect(configOf(connected).UNUSED_LENDING_POOL_IDS).toEqual([
|
|
163
|
-
'0xhidden',
|
|
164
|
-
]);
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
it('exposes the signer’s own provider on a connected instance', () => {
|
|
168
|
-
const wallet = signer();
|
|
169
|
-
const kasu = Kasu.create({ chain: 'base', signerOrProvider: wallet });
|
|
170
|
-
expect(kasu.provider).toBe(wallet.provider);
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
it('throws rather than returning undefined for a provider-less signer', () => {
|
|
174
|
-
const kasu = Kasu.create({
|
|
175
|
-
chain: 'base',
|
|
176
|
-
signerOrProvider: Wallet.createRandom(),
|
|
177
|
-
});
|
|
178
|
-
expect(() => kasu.provider).toThrow('has no provider attached');
|
|
179
|
-
});
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
describe('DepositsFacade — read-only writes', () => {
|
|
183
|
-
const readOnly = (): Kasu => Kasu.create({ chain: 'base' });
|
|
184
|
-
const READ_ONLY_MESSAGE =
|
|
185
|
-
'Kasu: this instance is read-only; call kasu.connect(signer) first';
|
|
186
|
-
|
|
187
|
-
it('refuses deposit before touching the contract', async () => {
|
|
188
|
-
await expect(
|
|
189
|
-
readOnly().deposits.deposit({
|
|
190
|
-
poolId: '0xpool',
|
|
191
|
-
trancheId: '0xtranche',
|
|
192
|
-
amount: 1,
|
|
193
|
-
kycSignature: { blockExpiration: 0, signature: '0x' },
|
|
194
|
-
}),
|
|
195
|
-
).rejects.toThrow(READ_ONLY_MESSAGE);
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
it('refuses withdraw', async () => {
|
|
199
|
-
await expect(
|
|
200
|
-
readOnly().deposits.withdraw({
|
|
201
|
-
poolId: '0xpool',
|
|
202
|
-
trancheId: '0xtranche',
|
|
203
|
-
amount: 1,
|
|
204
|
-
}),
|
|
205
|
-
).rejects.toThrow(READ_ONLY_MESSAGE);
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
it('refuses withdrawMax', async () => {
|
|
209
|
-
await expect(
|
|
210
|
-
readOnly().deposits.withdrawMax('0xpool', '0xtranche', '0xuser'),
|
|
211
|
-
).rejects.toThrow(READ_ONLY_MESSAGE);
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
it('lets a connected instance past the guard', () => {
|
|
215
|
-
const connected = readOnly().connect(signer());
|
|
216
|
-
// Reaches the contract call (and fails there on a fake address) rather
|
|
217
|
-
// than being refused up front. Probing the guard directly keeps the
|
|
218
|
-
// assertion off the network.
|
|
219
|
-
const guard = connected.deposits as unknown as {
|
|
220
|
-
assertWritable(): void;
|
|
221
|
-
};
|
|
222
|
-
expect(() => {
|
|
223
|
-
guard.assertWritable();
|
|
224
|
-
}).not.toThrow();
|
|
225
|
-
});
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
describe('Kasu.create — a ChainConfigEntry written before 2.5.0', () => {
|
|
229
|
-
// kasu-app-admin hand-builds one of these for base-sepolia. TypeScript will
|
|
230
|
-
// ask it for `stableAsset` and `rpcUrls` on bump; the runtime must not
|
|
231
|
-
// crash first.
|
|
232
|
-
const legacy = (): ChainConfigEntry =>
|
|
233
|
-
({
|
|
234
|
-
chainId: 84532,
|
|
235
|
-
name: 'base-sepolia',
|
|
236
|
-
isLiteDeployment: false,
|
|
237
|
-
contracts: BASE_CONTRACTS,
|
|
238
|
-
subgraphUrl: 'https://example.invalid/subgraph',
|
|
239
|
-
// Non-empty: `SdkConfig` documents `directusUrl` as optional, but
|
|
240
|
-
// `createDirectus('')` throws `TypeError: Invalid URL`. Pre-existing,
|
|
241
|
-
// and unrelated to what this test covers.
|
|
242
|
-
directusUrl: 'https://example.invalid/directus/',
|
|
243
|
-
unusedPoolIds: [''],
|
|
244
|
-
}) as unknown as ChainConfigEntry;
|
|
245
|
-
|
|
246
|
-
it('constructs with an explicit provider and falls back to 6 decimals', () => {
|
|
247
|
-
const kasu = Kasu.create({
|
|
248
|
-
chain: legacy(),
|
|
249
|
-
signerOrProvider: new providers.StaticJsonRpcProvider(
|
|
250
|
-
'https://example.invalid/rpc',
|
|
251
|
-
84532,
|
|
252
|
-
),
|
|
253
|
-
});
|
|
254
|
-
const config = (
|
|
255
|
-
kasu.services.DataService as unknown as { _kasuConfig: SdkConfig }
|
|
256
|
-
)._kasuConfig;
|
|
257
|
-
expect(config.stableAssetDecimals).toBe(6);
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
it('refuses a read-only create with the same message a retired chain gives', () => {
|
|
261
|
-
expect(() => Kasu.create({ chain: legacy() })).toThrow(
|
|
262
|
-
'Kasu.create: chain "base-sepolia" has no default RPC (retired); pass signerOrProvider',
|
|
263
|
-
);
|
|
264
|
-
});
|
|
265
|
-
});
|
|
@@ -1,374 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Offline facade specs — a mock provider and pure helpers, no network. Moved
|
|
3
|
-
* here from `src/tests/` so it runs beside the code it covers, and so the
|
|
4
|
-
* opt-in `LIVE_TESTS` gate on `src/tests/` does not take it out of CI.
|
|
5
|
-
*/
|
|
6
|
-
import { DataService } from '../services/DataService/data-service';
|
|
7
|
-
import { NO_DIRECTUS_URL_MESSAGE } from '../services/DataService/directus-client';
|
|
8
|
-
import { Portfolio } from '../services/Portfolio/portfolio';
|
|
9
|
-
import { UserRequestStatus } from '../services/UserLending/subgraph-types';
|
|
10
|
-
import { UserRequest } from '../services/UserLending/types';
|
|
11
|
-
import { UserLending } from '../services/UserLending/user-lending';
|
|
12
|
-
|
|
13
|
-
import { CHAIN_CONFIGS } from './chain-configs';
|
|
14
|
-
import { DepositsFacade } from './deposits';
|
|
15
|
-
import { Kasu } from './kasu';
|
|
16
|
-
import { StrategiesFacade } from './strategies';
|
|
17
|
-
import { PortfolioFacade } from './user-portfolio';
|
|
18
|
-
|
|
19
|
-
// ---------------------------------------------------------------------------
|
|
20
|
-
// Chain configs
|
|
21
|
-
// ---------------------------------------------------------------------------
|
|
22
|
-
|
|
23
|
-
describe('CHAIN_CONFIGS', () => {
|
|
24
|
-
it('should have base, xdc, and plume entries', () => {
|
|
25
|
-
expect(CHAIN_CONFIGS).toHaveProperty('base');
|
|
26
|
-
expect(CHAIN_CONFIGS).toHaveProperty('xdc');
|
|
27
|
-
expect(CHAIN_CONFIGS).toHaveProperty('plume');
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it('base should be a full deployment', () => {
|
|
31
|
-
const base = CHAIN_CONFIGS.base;
|
|
32
|
-
expect(base.chainId).toBe(8453);
|
|
33
|
-
expect(base.isLiteDeployment).toBe(false);
|
|
34
|
-
expect(base.contracts.KSUToken).toBeDefined();
|
|
35
|
-
expect(base.contracts.KasuNFTs).toBeDefined();
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it('xdc should be a lite deployment without KSU token', () => {
|
|
39
|
-
const xdc = CHAIN_CONFIGS.xdc;
|
|
40
|
-
expect(xdc.chainId).toBe(50);
|
|
41
|
-
expect(xdc.isLiteDeployment).toBe(true);
|
|
42
|
-
expect(xdc.contracts.KSUToken).toBeUndefined();
|
|
43
|
-
expect(xdc.contracts.KasuNFTs).toBeUndefined();
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it('plume should be a lite deployment', () => {
|
|
47
|
-
const plume = CHAIN_CONFIGS.plume;
|
|
48
|
-
expect(plume.chainId).toBe(98866);
|
|
49
|
-
expect(plume.isLiteDeployment).toBe(true);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('all chains should have required contract addresses', () => {
|
|
53
|
-
for (const [, config] of Object.entries(CHAIN_CONFIGS)) {
|
|
54
|
-
expect(config.contracts.LendingPoolManager).toBeDefined();
|
|
55
|
-
expect(config.contracts.UserManager).toBeDefined();
|
|
56
|
-
expect(config.contracts.KasuAllowList).toBeDefined();
|
|
57
|
-
expect(config.contracts.SystemVariables).toBeDefined();
|
|
58
|
-
expect(config.subgraphUrl).toContain('goldsky.com');
|
|
59
|
-
}
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it('plume points at the LEGACY goldsky project its frozen history is on', () => {
|
|
63
|
-
// Plume was indexed before the live chains moved to the current
|
|
64
|
-
// project; the same path under that project returns HTTP 404, so the
|
|
65
|
-
// retired deployment's history is only readable at this spelling —
|
|
66
|
-
// note the `/gn` suffix the current project's URLs do not carry.
|
|
67
|
-
const projectOf = (url: string): string => url.split('/')[5] ?? '';
|
|
68
|
-
expect(projectOf(CHAIN_CONFIGS.plume.subgraphUrl)).not.toBe(
|
|
69
|
-
projectOf(CHAIN_CONFIGS.base.subgraphUrl),
|
|
70
|
-
);
|
|
71
|
-
expect(CHAIN_CONFIGS.plume.subgraphUrl.endsWith('/gn')).toBe(true);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it('xdc should have poolMetadataMapping', () => {
|
|
75
|
-
const mapping = CHAIN_CONFIGS.xdc.poolMetadataMapping;
|
|
76
|
-
expect(mapping).toBeDefined();
|
|
77
|
-
expect(Object.keys(mapping ?? {}).length).toBeGreaterThan(0);
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
// ---------------------------------------------------------------------------
|
|
82
|
-
// Kasu.create()
|
|
83
|
-
// ---------------------------------------------------------------------------
|
|
84
|
-
|
|
85
|
-
describe('Kasu.create()', () => {
|
|
86
|
-
// Mock provider — enough to construct the SDK without network calls
|
|
87
|
-
const mockProvider = {
|
|
88
|
-
getNetwork: jest.fn().mockResolvedValue({ chainId: 8453, name: 'base' }),
|
|
89
|
-
call: jest.fn(),
|
|
90
|
-
estimateGas: jest.fn(),
|
|
91
|
-
getBlockNumber: jest.fn(),
|
|
92
|
-
getBlock: jest.fn(),
|
|
93
|
-
getTransaction: jest.fn(),
|
|
94
|
-
getTransactionReceipt: jest.fn(),
|
|
95
|
-
getLogs: jest.fn(),
|
|
96
|
-
on: jest.fn(),
|
|
97
|
-
once: jest.fn(),
|
|
98
|
-
off: jest.fn(),
|
|
99
|
-
removeAllListeners: jest.fn(),
|
|
100
|
-
_isProvider: true,
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
it('should create an instance from a chain name', () => {
|
|
104
|
-
const kasu = Kasu.create({
|
|
105
|
-
chain: 'base',
|
|
106
|
-
signerOrProvider: mockProvider as never,
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
expect(kasu).toBeInstanceOf(Kasu);
|
|
110
|
-
expect(kasu.strategies).toBeInstanceOf(StrategiesFacade);
|
|
111
|
-
expect(kasu.deposits).toBeInstanceOf(DepositsFacade);
|
|
112
|
-
expect(kasu.portfolio).toBeInstanceOf(PortfolioFacade);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it('should expose chainConfig for the selected chain', () => {
|
|
116
|
-
const kasu = Kasu.create({
|
|
117
|
-
chain: 'xdc',
|
|
118
|
-
signerOrProvider: mockProvider as never,
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
expect(kasu.chainConfig.chainId).toBe(50);
|
|
122
|
-
expect(kasu.isLiteDeployment).toBe(true);
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it('should accept a custom ChainConfigEntry', () => {
|
|
126
|
-
const custom = {
|
|
127
|
-
...CHAIN_CONFIGS.base,
|
|
128
|
-
chainId: 999,
|
|
129
|
-
name: 'CustomChain',
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
const kasu = Kasu.create({
|
|
133
|
-
chain: custom,
|
|
134
|
-
signerOrProvider: mockProvider as never,
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
expect(kasu.chainConfig.chainId).toBe(999);
|
|
138
|
-
expect(kasu.chainConfig.name).toBe('CustomChain');
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
it('should accept configOverrides', () => {
|
|
142
|
-
const kasu = Kasu.create({
|
|
143
|
-
chain: 'base',
|
|
144
|
-
signerOrProvider: mockProvider as never,
|
|
145
|
-
configOverrides: {
|
|
146
|
-
UNUSED_LENDING_POOL_IDS: ['0xdead'],
|
|
147
|
-
},
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
expect(kasu).toBeInstanceOf(Kasu);
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it('constructs without a directusUrl — on-chain data does not need one', () => {
|
|
154
|
-
// `createDirectus('')` throws `Invalid URL`, so an SDK configured
|
|
155
|
-
// without the (documented-optional) CMS URL used to be
|
|
156
|
-
// unconstructable rather than merely CMS-less.
|
|
157
|
-
const kasu = Kasu.create({
|
|
158
|
-
chain: 'base',
|
|
159
|
-
signerOrProvider: mockProvider as never,
|
|
160
|
-
configOverrides: { directusUrl: '' },
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
expect(kasu).toBeInstanceOf(Kasu);
|
|
164
|
-
expect(kasu.services.DataService).toBeDefined();
|
|
165
|
-
expect(kasu.services.UserLending).toBeDefined();
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
it('refuses a CMS-only call clearly when no directusUrl is configured', async () => {
|
|
169
|
-
const kasu = Kasu.create({
|
|
170
|
-
chain: 'base',
|
|
171
|
-
signerOrProvider: mockProvider as never,
|
|
172
|
-
configOverrides: { directusUrl: '' },
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
// A sentence naming what to configure, not a null dereference thrown
|
|
176
|
-
// from inside the vendor SDK.
|
|
177
|
-
await expect(
|
|
178
|
-
kasu.services.DataService.getPlatformOverview(),
|
|
179
|
-
).rejects.toThrow(NO_DIRECTUS_URL_MESSAGE);
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
it('should expose the underlying KasuSdk via .services', () => {
|
|
183
|
-
const kasu = Kasu.create({
|
|
184
|
-
chain: 'base',
|
|
185
|
-
signerOrProvider: mockProvider as never,
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
expect(kasu.services).toBeDefined();
|
|
189
|
-
expect(kasu.services.DataService).toBeDefined();
|
|
190
|
-
expect(kasu.services.UserLending).toBeDefined();
|
|
191
|
-
expect(kasu.services.Locking).toBeDefined();
|
|
192
|
-
expect(kasu.services.Portfolio).toBeDefined();
|
|
193
|
-
});
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
// ---------------------------------------------------------------------------
|
|
197
|
-
// StrategiesFacade.calculateDepositLimits()
|
|
198
|
-
// ---------------------------------------------------------------------------
|
|
199
|
-
|
|
200
|
-
describe('StrategiesFacade.calculateDepositLimits()', () => {
|
|
201
|
-
// We can test this pure function without network calls by constructing
|
|
202
|
-
// a StrategiesFacade with null services and calling calculateDepositLimits directly.
|
|
203
|
-
const facade = new StrategiesFacade(null as never, null as never);
|
|
204
|
-
|
|
205
|
-
it('should enforce minimum of 1 USDC', () => {
|
|
206
|
-
const tranche = makeTranche({
|
|
207
|
-
minimumDeposit: '0.5',
|
|
208
|
-
maximumDeposit: '10000',
|
|
209
|
-
poolCapacity: '50000',
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
const limits = facade.calculateDepositLimits(tranche);
|
|
213
|
-
expect(limits.min).toBe('1');
|
|
214
|
-
expect(limits.max).toBe('10000');
|
|
215
|
-
expect(limits.availableCapacity).toBe('50000');
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
it('should pass through minimum when >= 1', () => {
|
|
219
|
-
const tranche = makeTranche({
|
|
220
|
-
minimumDeposit: '100',
|
|
221
|
-
maximumDeposit: '10000',
|
|
222
|
-
poolCapacity: '50000',
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
const limits = facade.calculateDepositLimits(tranche);
|
|
226
|
-
expect(limits.min).toBe('100');
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
it('should cap max at pool capacity when capacity is lower', () => {
|
|
230
|
-
const tranche = makeTranche({
|
|
231
|
-
minimumDeposit: '100',
|
|
232
|
-
maximumDeposit: '50000',
|
|
233
|
-
poolCapacity: '10000',
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
const limits = facade.calculateDepositLimits(tranche);
|
|
237
|
-
expect(limits.max).toBe('10000');
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
it('should use maximumDeposit when capacity is higher', () => {
|
|
241
|
-
const tranche = makeTranche({
|
|
242
|
-
minimumDeposit: '100',
|
|
243
|
-
maximumDeposit: '5000',
|
|
244
|
-
poolCapacity: '50000',
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
const limits = facade.calculateDepositLimits(tranche);
|
|
248
|
-
expect(limits.max).toBe('5000');
|
|
249
|
-
});
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
// ---------------------------------------------------------------------------
|
|
253
|
-
// PortfolioFacade.getRequestStates()
|
|
254
|
-
// ---------------------------------------------------------------------------
|
|
255
|
-
|
|
256
|
-
describe('PortfolioFacade.getRequestStates()', () => {
|
|
257
|
-
it('maps the transaction history through deriveRequestState', async () => {
|
|
258
|
-
const requests: UserRequest[] = [
|
|
259
|
-
makeUserRequest({ id: 'req-1' }),
|
|
260
|
-
makeUserRequest({
|
|
261
|
-
id: 'req-2',
|
|
262
|
-
requestType: 'Withdrawal',
|
|
263
|
-
status: UserRequestStatus.PROCESSED,
|
|
264
|
-
canCancel: false,
|
|
265
|
-
acceptedAmount: '100',
|
|
266
|
-
}),
|
|
267
|
-
];
|
|
268
|
-
const userLending = {
|
|
269
|
-
getCurrentEpoch: (): Promise<string> => Promise.resolve('42'),
|
|
270
|
-
getUserRequests: (): Promise<UserRequest[]> =>
|
|
271
|
-
Promise.resolve(requests),
|
|
272
|
-
} as unknown as UserLending;
|
|
273
|
-
|
|
274
|
-
const facade = new PortfolioFacade(
|
|
275
|
-
{} as unknown as DataService,
|
|
276
|
-
userLending,
|
|
277
|
-
{} as unknown as Portfolio,
|
|
278
|
-
);
|
|
279
|
-
|
|
280
|
-
const states = await facade.getRequestStates('0xuser');
|
|
281
|
-
|
|
282
|
-
expect(states.map((s) => s.id)).toEqual(['req-1', 'req-2']);
|
|
283
|
-
expect(states[0].statusCode).toBe('pending');
|
|
284
|
-
expect(states[0].amount).toBe(100);
|
|
285
|
-
expect(states[1].statusCode).toBe('complete');
|
|
286
|
-
expect(states[1].amount).toBe(-100);
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
it('returns an empty list for a wallet with no history', async () => {
|
|
290
|
-
const userLending = {
|
|
291
|
-
getCurrentEpoch: (): Promise<string> => Promise.resolve('42'),
|
|
292
|
-
getUserRequests: (): Promise<UserRequest[]> => Promise.resolve([]),
|
|
293
|
-
} as unknown as UserLending;
|
|
294
|
-
|
|
295
|
-
const facade = new PortfolioFacade(
|
|
296
|
-
{} as unknown as DataService,
|
|
297
|
-
userLending,
|
|
298
|
-
{} as unknown as Portfolio,
|
|
299
|
-
);
|
|
300
|
-
|
|
301
|
-
await expect(facade.getRequestStates('0xuser')).resolves.toEqual([]);
|
|
302
|
-
});
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
// ---------------------------------------------------------------------------
|
|
306
|
-
// Type exports — verify key facade types are importable
|
|
307
|
-
// ---------------------------------------------------------------------------
|
|
308
|
-
|
|
309
|
-
describe('Type re-exports', () => {
|
|
310
|
-
it('should export facade classes from the main entry', () => {
|
|
311
|
-
// Verify the barrel exports resolve — these would fail at compile time
|
|
312
|
-
// if the re-exports were broken.
|
|
313
|
-
expect(Kasu).toBeDefined();
|
|
314
|
-
expect(CHAIN_CONFIGS).toBeDefined();
|
|
315
|
-
expect(StrategiesFacade).toBeDefined();
|
|
316
|
-
expect(DepositsFacade).toBeDefined();
|
|
317
|
-
expect(PortfolioFacade).toBeDefined();
|
|
318
|
-
});
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
// ---------------------------------------------------------------------------
|
|
322
|
-
// Helpers
|
|
323
|
-
// ---------------------------------------------------------------------------
|
|
324
|
-
|
|
325
|
-
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
|
326
|
-
function makeTranche(raw: {
|
|
327
|
-
minimumDeposit: string;
|
|
328
|
-
maximumDeposit: string;
|
|
329
|
-
poolCapacity: string;
|
|
330
|
-
}) {
|
|
331
|
-
return {
|
|
332
|
-
id: '0xtest',
|
|
333
|
-
name: 'Test Tranche',
|
|
334
|
-
apy: 0.08,
|
|
335
|
-
minApy: 0.06,
|
|
336
|
-
maxApy: 0.10,
|
|
337
|
-
minimumDeposit: raw.minimumDeposit,
|
|
338
|
-
maximumDeposit: raw.maximumDeposit,
|
|
339
|
-
availableCapacity: raw.poolCapacity,
|
|
340
|
-
fixedTermOptions: [],
|
|
341
|
-
_raw: {
|
|
342
|
-
...raw,
|
|
343
|
-
id: '0xtest',
|
|
344
|
-
name: 'Test Tranche',
|
|
345
|
-
apy: '0.08',
|
|
346
|
-
minApy: '0.06',
|
|
347
|
-
maxApy: '0.10',
|
|
348
|
-
fixedTermConfig: [],
|
|
349
|
-
},
|
|
350
|
-
} as never;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
function makeUserRequest(overrides: Partial<UserRequest> = {}): UserRequest {
|
|
355
|
-
return {
|
|
356
|
-
id: 'req-1',
|
|
357
|
-
userId: '0xuser',
|
|
358
|
-
lendingPool: { id: '0xpool', name: 'Pool', tranches: [{ orderId: '0' }] },
|
|
359
|
-
requestType: 'Deposit',
|
|
360
|
-
trancheId: '0xtranche',
|
|
361
|
-
trancheName: 'Senior',
|
|
362
|
-
requestedAmount: '100',
|
|
363
|
-
acceptedAmount: '0',
|
|
364
|
-
rejectedAmount: '0',
|
|
365
|
-
timestamp: 1_700_000_000,
|
|
366
|
-
status: UserRequestStatus.REQUESTED,
|
|
367
|
-
canCancel: true,
|
|
368
|
-
events: [],
|
|
369
|
-
nftId: '',
|
|
370
|
-
apy: '0',
|
|
371
|
-
fixedTermConfig: undefined,
|
|
372
|
-
...overrides,
|
|
373
|
-
};
|
|
374
|
-
}
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { providers } from 'ethers';
|
|
2
|
-
|
|
3
|
-
import { apyToEpochRate, epochRateToApy } from '../../domain/rates';
|
|
4
|
-
import { CHAIN_CONFIGS } from '../../facade/chain-configs';
|
|
5
|
-
import { SdkConfig } from '../../sdk-config';
|
|
6
|
-
|
|
7
|
-
import { KSULocking } from './locking';
|
|
8
|
-
|
|
9
|
-
// Offline: the constructor only builds contract instances. Nothing here calls
|
|
10
|
-
// the RPC or the subgraph.
|
|
11
|
-
function locking(): KSULocking {
|
|
12
|
-
const chain = CHAIN_CONFIGS.base;
|
|
13
|
-
const config = new SdkConfig({
|
|
14
|
-
subgraphUrl: 'https://example.invalid/subgraph',
|
|
15
|
-
contracts: chain.contracts,
|
|
16
|
-
UNUSED_LENDING_POOL_IDS: [''],
|
|
17
|
-
});
|
|
18
|
-
return new KSULocking(
|
|
19
|
-
config,
|
|
20
|
-
new providers.StaticJsonRpcProvider(chain.rpcUrls[0], chain.chainId),
|
|
21
|
-
);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
describe('KSULocking.calculateApy', () => {
|
|
25
|
-
it('compounds the epoch rate instead of XOR-ing it', () => {
|
|
26
|
-
// The old body was `(1 + r) ^ (EPOCHS_IN_YEAR - 1)` — bitwise XOR on
|
|
27
|
-
// int32 operands, so every realistic rate returned the constant 50.
|
|
28
|
-
for (const r of [0.001, 0.0025, 0.003, 0.005]) {
|
|
29
|
-
expect(locking().calculateApy(r)).toBe(epochRateToApy(r));
|
|
30
|
-
expect(locking().calculateApy(r)).not.toBe(50);
|
|
31
|
-
}
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it('lands on the APYs the platform actually quotes', () => {
|
|
35
|
-
// Every live tranche APY, back through its own per-epoch rate.
|
|
36
|
-
for (const apy of [0.1, 0.12, 0.14, 0.16, 0.22, 0.3]) {
|
|
37
|
-
expect(locking().calculateApy(apyToEpochRate(apy))).toBeCloseTo(
|
|
38
|
-
apy,
|
|
39
|
-
10,
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
expect(locking().calculateApy(0)).toBe(0);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('agrees with DataService.calculateApyForTranche', () => {
|
|
46
|
-
// The two used to carry separate copies of the constant AND separate
|
|
47
|
-
// (one of them wrong) formulas.
|
|
48
|
-
for (const r of [0.001, 0.003, 0.01]) {
|
|
49
|
-
expect(locking().calculateApy(r)).toBe(epochRateToApy(r));
|
|
50
|
-
}
|
|
51
|
-
});
|
|
52
|
-
});
|