@kasufinance/kasu-sdk 2.4.2 → 2.5.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/LICENSE +21 -0
- package/README.md +189 -253
- package/dist/bundle.cjs.js +4775 -1794
- package/dist/bundle.esm.js +4764 -1816
- package/dist/domain/deposit-bounds.d.ts +62 -0
- package/dist/domain/deposit-bounds.js +84 -0
- package/dist/domain/deposit-bounds.js.map +1 -0
- package/dist/domain/deposit-bounds.test.js +135 -0
- package/dist/domain/deposit-bounds.test.js.map +1 -0
- package/dist/domain/index.d.ts +26 -0
- package/dist/domain/index.js +23 -0
- package/dist/domain/index.js.map +1 -0
- package/dist/domain/partners.d.ts +34 -0
- package/dist/domain/partners.js +43 -0
- package/dist/domain/partners.js.map +1 -0
- package/dist/domain/partners.test.d.ts +1 -0
- package/dist/domain/partners.test.js +53 -0
- package/dist/domain/partners.test.js.map +1 -0
- package/dist/domain/pools.d.ts +70 -0
- package/dist/domain/pools.js +134 -0
- package/dist/domain/pools.js.map +1 -0
- package/dist/domain/pools.test.d.ts +1 -0
- package/dist/domain/pools.test.js +184 -0
- package/dist/domain/pools.test.js.map +1 -0
- package/dist/domain/rates.d.ts +83 -0
- package/dist/domain/rates.js +120 -0
- package/dist/domain/rates.js.map +1 -0
- package/dist/domain/rates.test.d.ts +1 -0
- package/dist/domain/rates.test.js +181 -0
- package/dist/domain/rates.test.js.map +1 -0
- package/dist/domain/tranche-display-name.d.ts +17 -0
- package/dist/domain/tranche-display-name.js +30 -0
- package/dist/domain/tranche-display-name.js.map +1 -0
- package/dist/domain/tranche-display-name.test.d.ts +1 -0
- package/dist/domain/tranche-display-name.test.js +58 -0
- package/dist/domain/tranche-display-name.test.js.map +1 -0
- package/dist/domain/tranches.d.ts +89 -0
- package/dist/domain/tranches.js +150 -0
- package/dist/domain/tranches.js.map +1 -0
- package/dist/domain/tranches.test.d.ts +1 -0
- package/dist/domain/tranches.test.js +206 -0
- package/dist/domain/tranches.test.js.map +1 -0
- package/dist/facade/chain-configs.js +54 -0
- package/dist/facade/chain-configs.js.map +1 -1
- package/dist/facade/config.test.d.ts +1 -0
- package/dist/facade/config.test.js +216 -0
- package/dist/facade/config.test.js.map +1 -0
- package/dist/facade/deposits.d.ts +22 -1
- package/dist/facade/deposits.js +24 -1
- package/dist/facade/deposits.js.map +1 -1
- package/dist/facade/facade.test.d.ts +1 -0
- package/dist/{tests → facade}/facade.test.js +10 -5
- package/dist/facade/facade.test.js.map +1 -0
- package/dist/facade/index.d.ts +2 -1
- package/dist/facade/index.js +2 -0
- package/dist/facade/index.js.map +1 -1
- package/dist/facade/kasu.d.ts +49 -8
- package/dist/facade/kasu.js +117 -13
- package/dist/facade/kasu.js.map +1 -1
- package/dist/facade/strategies.d.ts +25 -0
- package/dist/facade/strategies.js +36 -0
- package/dist/facade/strategies.js.map +1 -1
- package/dist/facade/types.d.ts +57 -2
- package/dist/facade/unused-pool-ids.d.ts +31 -0
- package/dist/facade/unused-pool-ids.js +57 -0
- package/dist/facade/unused-pool-ids.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/sdk-config.d.ts +11 -0
- package/dist/sdk-config.js +8 -1
- package/dist/sdk-config.js.map +1 -1
- package/dist/services/DataService/data-service.d.ts +5 -0
- package/dist/services/DataService/data-service.js +7 -2
- package/dist/services/DataService/data-service.js.map +1 -1
- package/dist/services/Locking/calculate-apy.test.d.ts +1 -0
- package/dist/services/Locking/calculate-apy.test.js +41 -0
- package/dist/services/Locking/calculate-apy.test.js.map +1 -0
- package/dist/services/Locking/locking.d.ts +14 -0
- package/dist/services/Locking/locking.js +16 -2
- package/dist/services/Locking/locking.js.map +1 -1
- package/dist/tests/sample.test.js +46 -25
- package/dist/tests/sample.test.js.map +1 -1
- package/package.json +43 -12
- package/src/domain/deposit-bounds.test.ts +200 -0
- package/src/domain/deposit-bounds.ts +118 -0
- package/src/domain/index.ts +70 -0
- package/src/domain/partners.test.ts +83 -0
- package/src/domain/partners.ts +66 -0
- package/src/domain/pools.test.ts +260 -0
- package/src/domain/pools.ts +150 -0
- package/src/domain/rates.test.ts +254 -0
- package/src/domain/rates.ts +120 -0
- package/src/domain/tranche-display-name.test.ts +96 -0
- package/src/domain/tranche-display-name.ts +34 -0
- package/src/domain/tranches.test.ts +300 -0
- package/src/domain/tranches.ts +177 -0
- package/src/facade/chain-configs.ts +55 -0
- package/src/facade/config.test.ts +265 -0
- package/src/facade/deposits.ts +25 -0
- package/src/{tests → facade}/facade.test.ts +10 -5
- package/src/facade/index.ts +4 -0
- package/src/facade/kasu.ts +155 -12
- package/src/facade/strategies.ts +36 -0
- package/src/facade/types.ts +60 -2
- package/src/facade/unused-pool-ids.ts +59 -0
- package/src/index.ts +6 -0
- package/src/sdk-config.ts +19 -1
- package/src/services/DataService/data-service.ts +7 -2
- package/src/services/Locking/calculate-apy.test.ts +52 -0
- package/src/services/Locking/locking.ts +16 -2
- package/src/tests/sample.test.ts +54 -33
- package/.eslintignore +0 -3
- package/.eslintrc.json +0 -90
- package/.github/workflows/ci.yml +0 -39
- package/.github/workflows/gitleaks.yml +0 -22
- package/.github/workflows/main.yml +0 -41
- package/.gitleaksignore +0 -19
- package/.prettierignore +0 -3
- package/.prettierrc +0 -28
- package/CLAUDE.md +0 -410
- package/copy-abi.ps1 +0 -15
- package/dist/tests/facade.test.js.map +0 -1
- package/index.ts +0 -2
- package/jest-config.ts +0 -9
- package/jest.config.js +0 -8
- package/rollup.config.js +0 -45
- package/tsconfig.json +0 -27
- /package/dist/{tests/facade.test.d.ts → domain/deposit-bounds.test.d.ts} +0 -0
package/src/facade/kasu.ts
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
|
+
import { Provider } from '@ethersproject/providers';
|
|
2
|
+
import { providers, Signer } from 'ethers';
|
|
3
|
+
|
|
1
4
|
import { KasuSdk } from '../kasu-sdk';
|
|
2
|
-
import { SdkConfig } from '../sdk-config';
|
|
5
|
+
import { SdkConfig, SdkConfigOptions } from '../sdk-config';
|
|
3
6
|
|
|
4
7
|
import { CHAIN_CONFIGS } from './chain-configs';
|
|
5
8
|
import { DepositsFacade } from './deposits';
|
|
6
9
|
import { StrategiesFacade } from './strategies';
|
|
7
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
ChainConfigEntry,
|
|
12
|
+
KasuOptions,
|
|
13
|
+
StableAsset,
|
|
14
|
+
SupportedChain,
|
|
15
|
+
} from './types';
|
|
8
16
|
import { PortfolioFacade } from './user-portfolio';
|
|
9
17
|
|
|
10
18
|
/**
|
|
@@ -16,15 +24,15 @@ import { PortfolioFacade } from './user-portfolio';
|
|
|
16
24
|
* ```ts
|
|
17
25
|
* import { Kasu } from '@kasufinance/kasu-sdk';
|
|
18
26
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* // Browse strategies
|
|
27
|
+
* // Read-only — no wallet needed, uses the chain's default RPC
|
|
28
|
+
* const kasu = Kasu.create({ chain: 'base' });
|
|
22
29
|
* const strategies = await kasu.strategies.getAll();
|
|
23
30
|
*
|
|
24
|
-
* //
|
|
25
|
-
* const
|
|
31
|
+
* // Writable — same config, bound to a signer
|
|
32
|
+
* const signed = kasu.connect(signer);
|
|
33
|
+
* const tx = await signed.deposits.deposit({ poolId, trancheId, amount, kycSignature });
|
|
26
34
|
*
|
|
27
|
-
* // User positions
|
|
35
|
+
* // User positions (read-only is enough)
|
|
28
36
|
* const positions = await kasu.portfolio.getPositions(userAddress);
|
|
29
37
|
* ```
|
|
30
38
|
*/
|
|
@@ -38,13 +46,24 @@ export class Kasu {
|
|
|
38
46
|
|
|
39
47
|
private readonly _sdk: KasuSdk;
|
|
40
48
|
private readonly _chainConfig: ChainConfigEntry;
|
|
49
|
+
private readonly _signerOrProvider: Provider | Signer;
|
|
50
|
+
/**
|
|
51
|
+
* Kept so `connect(signer)` can rebuild an identical instance. A connected
|
|
52
|
+
* instance that silently dropped the overrides would query a different set
|
|
53
|
+
* of pools than the read-only one it came from.
|
|
54
|
+
*/
|
|
55
|
+
private readonly _configOverrides: Partial<SdkConfigOptions>;
|
|
41
56
|
|
|
42
57
|
private constructor(
|
|
43
58
|
sdk: KasuSdk,
|
|
44
59
|
chainConfig: ChainConfigEntry,
|
|
60
|
+
signerOrProvider: Provider | Signer,
|
|
61
|
+
configOverrides: Partial<SdkConfigOptions>,
|
|
45
62
|
) {
|
|
46
63
|
this._sdk = sdk;
|
|
47
64
|
this._chainConfig = chainConfig;
|
|
65
|
+
this._signerOrProvider = signerOrProvider;
|
|
66
|
+
this._configOverrides = configOverrides;
|
|
48
67
|
|
|
49
68
|
this.strategies = new StrategiesFacade(
|
|
50
69
|
sdk.DataService,
|
|
@@ -54,6 +73,7 @@ export class Kasu {
|
|
|
54
73
|
this.deposits = new DepositsFacade(
|
|
55
74
|
sdk.UserLending,
|
|
56
75
|
chainConfig.chainId.toString(),
|
|
76
|
+
!Signer.isSigner(signerOrProvider),
|
|
57
77
|
);
|
|
58
78
|
|
|
59
79
|
this.portfolio = new PortfolioFacade(
|
|
@@ -66,9 +86,12 @@ export class Kasu {
|
|
|
66
86
|
/**
|
|
67
87
|
* Create a Kasu instance with built-in chain config.
|
|
68
88
|
*
|
|
89
|
+
* `signerOrProvider` is optional: without one the instance is READ-ONLY on
|
|
90
|
+
* `chainConfig.rpcUrls[0]`. Use `connect(signer)` when a wallet arrives.
|
|
91
|
+
*
|
|
69
92
|
* ```ts
|
|
70
|
-
* // Minimal setup —
|
|
71
|
-
* const kasu = Kasu.create({ chain: 'base'
|
|
93
|
+
* // Minimal setup — read-only, built-in Base mainnet config
|
|
94
|
+
* const kasu = Kasu.create({ chain: 'base' });
|
|
72
95
|
*
|
|
73
96
|
* // Custom config override
|
|
74
97
|
* const kasu = Kasu.create({
|
|
@@ -102,10 +125,46 @@ export class Kasu {
|
|
|
102
125
|
poolMetadataMapping:
|
|
103
126
|
overrides.poolMetadataMapping ??
|
|
104
127
|
chainConfig.poolMetadataMapping,
|
|
128
|
+
// Follow the chain's own token rather than `SdkConfig`'s default
|
|
129
|
+
// of 6. Every live deployment happens to be 6dp today, so the
|
|
130
|
+
// default was right by luck; the next one need not be.
|
|
131
|
+
stableAssetDecimals:
|
|
132
|
+
overrides.stableAssetDecimals ??
|
|
133
|
+
stableAssetOf(chainConfig)?.decimals,
|
|
105
134
|
});
|
|
106
135
|
|
|
107
|
-
const
|
|
108
|
-
|
|
136
|
+
const signerOrProvider =
|
|
137
|
+
options.signerOrProvider ??
|
|
138
|
+
defaultProvider(
|
|
139
|
+
chainConfig,
|
|
140
|
+
typeof options.chain === 'string'
|
|
141
|
+
? options.chain
|
|
142
|
+
: chainConfig.name,
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
const sdk = new KasuSdk(sdkConfig, signerOrProvider);
|
|
146
|
+
return new Kasu(sdk, chainConfig, signerOrProvider, overrides);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* A NEW instance bound to `signer`, with the same chain config and the same
|
|
151
|
+
* `configOverrides` — the writable counterpart of a read-only instance.
|
|
152
|
+
*
|
|
153
|
+
* Named after `contract.connect(signer)` in ethers, and behaves the same
|
|
154
|
+
* way: the receiver is not mutated, so a read-only `Kasu` stays read-only
|
|
155
|
+
* and can keep serving public data after a wallet connects.
|
|
156
|
+
*
|
|
157
|
+
* ```ts
|
|
158
|
+
* const kasu = Kasu.create({ chain: 'base' }); // read-only
|
|
159
|
+
* const signed = kasu.connect(await getSigner()); // writable
|
|
160
|
+
* ```
|
|
161
|
+
*/
|
|
162
|
+
connect(signer: Signer): Kasu {
|
|
163
|
+
return Kasu.create({
|
|
164
|
+
chain: this._chainConfig,
|
|
165
|
+
signerOrProvider: signer,
|
|
166
|
+
configOverrides: this._configOverrides,
|
|
167
|
+
});
|
|
109
168
|
}
|
|
110
169
|
|
|
111
170
|
/**
|
|
@@ -128,12 +187,96 @@ export class Kasu {
|
|
|
128
187
|
get isLiteDeployment(): boolean {
|
|
129
188
|
return this._chainConfig.isLiteDeployment;
|
|
130
189
|
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* True when this instance was built from a Provider rather than a Signer,
|
|
193
|
+
* so every read works and every write is refused. Call `connect(signer)`
|
|
194
|
+
* for a writable instance.
|
|
195
|
+
*/
|
|
196
|
+
get isReadOnly(): boolean {
|
|
197
|
+
return !Signer.isSigner(this._signerOrProvider);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* The provider this instance reads through — the signer's own provider when
|
|
202
|
+
* it was created from a signer.
|
|
203
|
+
*
|
|
204
|
+
* Throws for a signer with no provider attached (an offline signer can sign
|
|
205
|
+
* but cannot read), because there is no provider to hand back and returning
|
|
206
|
+
* `undefined` would only move the failure somewhere less obvious.
|
|
207
|
+
*/
|
|
208
|
+
get provider(): Provider {
|
|
209
|
+
if (Signer.isSigner(this._signerOrProvider)) {
|
|
210
|
+
const provider = this._signerOrProvider.provider;
|
|
211
|
+
if (!provider) {
|
|
212
|
+
throw new Error(
|
|
213
|
+
'Kasu: the signer this instance was created with has no provider attached',
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
return provider;
|
|
217
|
+
}
|
|
218
|
+
return this._signerOrProvider;
|
|
219
|
+
}
|
|
131
220
|
}
|
|
132
221
|
|
|
133
222
|
// ---------------------------------------------------------------------------
|
|
134
223
|
// Helpers
|
|
135
224
|
// ---------------------------------------------------------------------------
|
|
136
225
|
|
|
226
|
+
// `stableAsset` and `rpcUrls` are REQUIRED on `ChainConfigEntry`, so every
|
|
227
|
+
// built-in config and every entry written against 2.5.0 carries them. A custom
|
|
228
|
+
// entry hand-built against an older release does not, and a consumer bumping to
|
|
229
|
+
// 2.5.0 must not crash on `undefined.decimals` before TypeScript has told them
|
|
230
|
+
// what to add (kasu-app-admin builds one for base-sepolia). Both readers below
|
|
231
|
+
// take the field as possibly absent and fall back: the stable-asset decimals to
|
|
232
|
+
// `SdkConfig`'s own default of 6, the RPC list to empty — which makes a
|
|
233
|
+
// read-only `create` throw the same clear message a retired chain does.
|
|
234
|
+
function stableAssetOf(
|
|
235
|
+
chainConfig: ChainConfigEntry,
|
|
236
|
+
): StableAsset | undefined {
|
|
237
|
+
return (chainConfig as { stableAsset?: StableAsset }).stableAsset;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function rpcUrlsOf(chainConfig: ChainConfigEntry): string[] {
|
|
241
|
+
return (chainConfig as { rpcUrls?: string[] }).rpcUrls ?? [];
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* The read-only provider used when the caller passes no `signerOrProvider`.
|
|
246
|
+
*
|
|
247
|
+
* `StaticJsonRpcProvider` rather than `JsonRpcProvider`: the chain id is known
|
|
248
|
+
* from the config, so it skips the `eth_chainId` round trip on every call and
|
|
249
|
+
* never re-detects the network — the right choice for an endpoint that serves
|
|
250
|
+
* exactly one chain.
|
|
251
|
+
*
|
|
252
|
+
* Only `rpcUrls[0]` is used. The list is a starting preference, not a failover
|
|
253
|
+
* strategy: an app that needs failover builds its own provider and passes it
|
|
254
|
+
* in, and baking a retry policy in here would hide outages from the app that
|
|
255
|
+
* has to report them.
|
|
256
|
+
*
|
|
257
|
+
* Constructed through the `ethers` namespace rather than by importing
|
|
258
|
+
* `StaticJsonRpcProvider` from `@ethersproject/providers` directly. `ethers` is
|
|
259
|
+
* the only provider package the rollup config marks external, so this picks up
|
|
260
|
+
* the CONSUMER's build — the Node one under Node, the browser one in a browser.
|
|
261
|
+
* Importing the class directly would inline the browser transport into the
|
|
262
|
+
* bundle, and its `fetch` call carries `referrer: 'client'`, which Node rejects
|
|
263
|
+
* outright ("Referrer \"client\" is not a valid URL"). It would also give the
|
|
264
|
+
* SDK a second provider implementation, so `instanceof` against the consumer's
|
|
265
|
+
* ethers would quietly fail.
|
|
266
|
+
*/
|
|
267
|
+
function defaultProvider(
|
|
268
|
+
chainConfig: ChainConfigEntry,
|
|
269
|
+
chainLabel: string,
|
|
270
|
+
): Provider {
|
|
271
|
+
const url = rpcUrlsOf(chainConfig)[0];
|
|
272
|
+
if (!url) {
|
|
273
|
+
throw new Error(
|
|
274
|
+
`Kasu.create: chain "${chainLabel}" has no default RPC (retired); pass signerOrProvider`,
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
return new providers.StaticJsonRpcProvider(url, chainConfig.chainId);
|
|
278
|
+
}
|
|
279
|
+
|
|
137
280
|
function resolveChainConfig(
|
|
138
281
|
chain: SupportedChain | ChainConfigEntry,
|
|
139
282
|
): ChainConfigEntry {
|
package/src/facade/strategies.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { selectVisiblePools } from '../domain/pools';
|
|
1
2
|
import { DataService } from '../services/DataService/data-service';
|
|
2
3
|
import { PoolOverview } from '../services/DataService/types';
|
|
3
4
|
import { UserLending } from '../services/UserLending/user-lending';
|
|
@@ -34,6 +35,41 @@ export class StrategiesFacade {
|
|
|
34
35
|
return pools.map((pool) => this.mapPoolToStrategy(pool));
|
|
35
36
|
}
|
|
36
37
|
|
|
38
|
+
/**
|
|
39
|
+
* The strategies a lender should actually see: active, not oversubscribed,
|
|
40
|
+
* with the ones that still have capacity first and the highest max APY
|
|
41
|
+
* next. `getAll` returns everything the subgraph knows about, including
|
|
42
|
+
* pools that are wound down.
|
|
43
|
+
*
|
|
44
|
+
* The filter runs on the raw `PoolOverview[]` BEFORE mapping, so `_raw` on
|
|
45
|
+
* every returned `Strategy` is the untouched pool object.
|
|
46
|
+
*/
|
|
47
|
+
async getVisible(poolIds?: string[]): Promise<Strategy[]> {
|
|
48
|
+
const epochId = await this._userLending.getCurrentEpoch();
|
|
49
|
+
const pools = await this._dataService.getPoolOverview(epochId, poolIds);
|
|
50
|
+
return selectVisiblePools(pools).map((pool) =>
|
|
51
|
+
this.mapPoolToStrategy(pool),
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The chain's performance fee.
|
|
57
|
+
*
|
|
58
|
+
* ⚠️ A PERCENTAGE IN 0..100 (`10` means ten percent), NOT a fraction —
|
|
59
|
+
* feed it to `netEffectiveApy` as-is. Treating it as `0.10` computes a
|
|
60
|
+
* nonsense negative rate that still renders as a plausible-looking
|
|
61
|
+
* percentage, which is why the units are stated here and in
|
|
62
|
+
* `netEffectiveApy`'s own JSDoc.
|
|
63
|
+
*
|
|
64
|
+
* ```ts
|
|
65
|
+
* const feePercent = await kasu.strategies.getPerformanceFeePercent();
|
|
66
|
+
* const net = netEffectiveApy(strategy.tranches[0].apy, feePercent);
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
async getPerformanceFeePercent(): Promise<number> {
|
|
70
|
+
return await this._dataService.getPerformanceFee();
|
|
71
|
+
}
|
|
72
|
+
|
|
37
73
|
/**
|
|
38
74
|
* Fetch a single strategy by pool ID. Returns `null` if not found.
|
|
39
75
|
*/
|
package/src/facade/types.ts
CHANGED
|
@@ -13,6 +13,34 @@ import {
|
|
|
13
13
|
|
|
14
14
|
export type SupportedChain = 'base' | 'xdc' | 'xdc-usdc' | 'plume';
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* The single stable token a deployment lends in. There is exactly ONE per
|
|
18
|
+
* deployment and it never changes: a different stable token is a separate full
|
|
19
|
+
* deployment (the XDC AUDD / XDC USDC pattern), never a companion vault on an
|
|
20
|
+
* existing one. That is why this is a plain object on the chain config rather
|
|
21
|
+
* than a list.
|
|
22
|
+
*/
|
|
23
|
+
export interface StableAsset {
|
|
24
|
+
/** ERC-20 address of the stable token on this chain. */
|
|
25
|
+
address: string;
|
|
26
|
+
/** Ticker as the token contract reports it (`USDC`, `AUDD`, `pUSD`). */
|
|
27
|
+
symbol: string;
|
|
28
|
+
/** Token name as the contract reports it (`USD Coin`). */
|
|
29
|
+
name: string;
|
|
30
|
+
/**
|
|
31
|
+
* Token decimals. Feeds `SdkConfig.stableAssetDecimals`, so every
|
|
32
|
+
* `parseUnits`/`formatUnits` in the SDK follows the chain rather than the
|
|
33
|
+
* hard-coded default of 6.
|
|
34
|
+
*/
|
|
35
|
+
decimals: number;
|
|
36
|
+
/**
|
|
37
|
+
* ISO-4217 code of the fiat currency the token tracks (`USD`, `AUD`).
|
|
38
|
+
* A CODE, not copy: consumers pick their own symbol and locale from it.
|
|
39
|
+
* Nothing in this SDK formats it.
|
|
40
|
+
*/
|
|
41
|
+
currencyCode: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
16
44
|
export interface ChainConfigEntry {
|
|
17
45
|
chainId: number;
|
|
18
46
|
name: string;
|
|
@@ -20,16 +48,46 @@ export interface ChainConfigEntry {
|
|
|
20
48
|
contracts: ContractAddresses;
|
|
21
49
|
subgraphUrl: string;
|
|
22
50
|
directusUrl: string;
|
|
51
|
+
/**
|
|
52
|
+
* Pools to hide. An EMPTY array is normalised to `['']` by `SdkConfig` —
|
|
53
|
+
* the subgraph reads `id_not_in: []` as "match nothing" and returns zero
|
|
54
|
+
* pools. See `SdkConfigOptions.UNUSED_LENDING_POOL_IDS`.
|
|
55
|
+
*/
|
|
23
56
|
unusedPoolIds: string[];
|
|
24
57
|
poolMetadataMapping?: Record<string, string>;
|
|
58
|
+
/** The one stable token this deployment lends in. */
|
|
59
|
+
stableAsset: StableAsset;
|
|
60
|
+
/**
|
|
61
|
+
* Public RPC endpoints for read-only use, in STARTING preference order
|
|
62
|
+
* only. Apps are expected to override with their own paid/keyed endpoints
|
|
63
|
+
* and their own failover; `Kasu.create` uses `rpcUrls[0]` and nothing else
|
|
64
|
+
* when no `signerOrProvider` is passed.
|
|
65
|
+
*
|
|
66
|
+
* Empty on a retired deployment, which has no read-only default.
|
|
67
|
+
*/
|
|
68
|
+
rpcUrls: string[];
|
|
69
|
+
/**
|
|
70
|
+
* True for a wound-down deployment kept only as frozen history. It has no
|
|
71
|
+
* default RPC, so a read-only `Kasu.create` on it throws.
|
|
72
|
+
*/
|
|
73
|
+
retired?: boolean;
|
|
25
74
|
}
|
|
26
75
|
|
|
27
76
|
/** Options passed to `Kasu.create()`. */
|
|
28
77
|
export interface KasuOptions {
|
|
29
78
|
/** A supported chain name or a custom `ChainConfigEntry`. */
|
|
30
79
|
chain: SupportedChain | ChainConfigEntry;
|
|
31
|
-
/**
|
|
32
|
-
|
|
80
|
+
/**
|
|
81
|
+
* ethers Signer (for transactions) or Provider (read-only).
|
|
82
|
+
*
|
|
83
|
+
* OPTIONAL. When omitted, `Kasu.create` builds a read-only
|
|
84
|
+
* `StaticJsonRpcProvider` on `chainConfig.rpcUrls[0]`, so browsing
|
|
85
|
+
* strategies and platform stats needs no wallet at all. Get a writable
|
|
86
|
+
* instance later with `kasu.connect(signer)`.
|
|
87
|
+
*/
|
|
88
|
+
signerOrProvider?:
|
|
89
|
+
| import('ethers').Signer
|
|
90
|
+
| import('@ethersproject/providers').Provider;
|
|
33
91
|
/** Override any default config value. */
|
|
34
92
|
configOverrides?: Partial<SdkConfigOptions>;
|
|
35
93
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/** Directus instance holding Kasu's pool editorial content. */
|
|
2
|
+
const DEFAULT_DIRECTUS_URL = 'https://kasu-finance.directus.app/';
|
|
3
|
+
|
|
4
|
+
interface PoolOverviewIdRow {
|
|
5
|
+
id?: unknown;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface PoolOverviewIdResponse {
|
|
9
|
+
data?: PoolOverviewIdRow[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The pool ids that are configured but NOT enabled — the ones every query
|
|
14
|
+
* should exclude.
|
|
15
|
+
*
|
|
16
|
+
* "Enabled" is editorial state, held in Directus rather than on-chain: a pool
|
|
17
|
+
* exists in the subgraph from the moment it is deployed, and stays hidden
|
|
18
|
+
* until content has been written for it. Every consumer has been hard-coding
|
|
19
|
+
* this list; reading it means a pool goes live without a frontend release.
|
|
20
|
+
*
|
|
21
|
+
* ```ts
|
|
22
|
+
* const kasu = Kasu.create({
|
|
23
|
+
* chain: 'base',
|
|
24
|
+
* configOverrides: { UNUSED_LENDING_POOL_IDS: await fetchUnusedPoolIds() },
|
|
25
|
+
* });
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* May legitimately return an empty array (every pool enabled). Pass it
|
|
29
|
+
* straight through: `SdkConfig` normalises an empty list to the `['']`
|
|
30
|
+
* sentinel, because the subgraph reads `id_not_in: []` as "match nothing" and
|
|
31
|
+
* would hide every pool.
|
|
32
|
+
*
|
|
33
|
+
* Uses the global `fetch`, so it needs Node 18+ or a browser. This is I/O and
|
|
34
|
+
* deliberately not part of `domain/`.
|
|
35
|
+
*
|
|
36
|
+
* @param directusUrl base URL of the Directus instance, with or without a
|
|
37
|
+
* trailing slash.
|
|
38
|
+
* @throws when the request fails or Directus answers with a non-2xx status —
|
|
39
|
+
* never silently returns `[]`, which would be indistinguishable from "every
|
|
40
|
+
* pool is enabled" and would un-hide pools that have no content.
|
|
41
|
+
*/
|
|
42
|
+
export async function fetchUnusedPoolIds(
|
|
43
|
+
directusUrl: string = DEFAULT_DIRECTUS_URL,
|
|
44
|
+
): Promise<string[]> {
|
|
45
|
+
const base = directusUrl.endsWith('/') ? directusUrl : `${directusUrl}/`;
|
|
46
|
+
const url = `${base}items/PoolOverview?filter[enabled][_neq]=true`;
|
|
47
|
+
|
|
48
|
+
const response = await fetch(url);
|
|
49
|
+
if (!response.ok) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
`fetchUnusedPoolIds: Directus responded ${String(response.status)} ${response.statusText} for ${url}`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const body = (await response.json()) as PoolOverviewIdResponse;
|
|
56
|
+
return (body.data ?? [])
|
|
57
|
+
.map((row) => row.id)
|
|
58
|
+
.filter((id): id is string => typeof id === 'string' && id.length > 0);
|
|
59
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -59,3 +59,9 @@ export type {
|
|
|
59
59
|
// Re-export facade (high-level integrator API)
|
|
60
60
|
// ---------------------------------------------------------------------------
|
|
61
61
|
export * from './facade';
|
|
62
|
+
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// Shared domain layer — pure rate, tranche and pool rules. Numbers and codes
|
|
65
|
+
// only: no copy, no locale. See `src/domain/index.ts`.
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
export * from './domain';
|
package/src/sdk-config.ts
CHANGED
|
@@ -20,6 +20,17 @@ export interface SdkConfigOptions {
|
|
|
20
20
|
contracts: ContractAddresses;
|
|
21
21
|
/** Directus CMS URL. Optional – when omitted, pool descriptions/images will not be available but on-chain data still works. */
|
|
22
22
|
directusUrl?: string;
|
|
23
|
+
/**
|
|
24
|
+
* Pool ids to EXCLUDE from every subgraph query (`id_not_in`).
|
|
25
|
+
*
|
|
26
|
+
* An empty array is normalised to `['']` by the `SdkConfig` constructor.
|
|
27
|
+
* The subgraph reads `id_not_in: []` as "match nothing", not "exclude
|
|
28
|
+
* nothing": on Base, `[]` returns 0 pools where `['']` returns all 9
|
|
29
|
+
* (verified live 2026-09-04). Passing `[]` therefore used to make the SDK
|
|
30
|
+
* silently return an empty platform, which is why every consumer already
|
|
31
|
+
* passes a sentinel of its own. A sentinel you pass yourself is left
|
|
32
|
+
* untouched.
|
|
33
|
+
*/
|
|
23
34
|
UNUSED_LENDING_POOL_IDS: string[];
|
|
24
35
|
/**
|
|
25
36
|
* Whether this is a Lite deployment (no KSU token, locking, or loyalty features).
|
|
@@ -55,7 +66,14 @@ export class SdkConfig {
|
|
|
55
66
|
this.subgraphUrl = options.subgraphUrl;
|
|
56
67
|
this.contracts = options.contracts;
|
|
57
68
|
this.directusUrl = options.directusUrl ?? '';
|
|
58
|
-
|
|
69
|
+
// `id_not_in: []` matches NOTHING in the subgraph, so an empty
|
|
70
|
+
// exclusion list would hide every pool. The empty-string sentinel is
|
|
71
|
+
// the "exclude nothing" spelling — no pool id is the empty string, and
|
|
72
|
+
// it is what the consumers already work around this with.
|
|
73
|
+
this.UNUSED_LENDING_POOL_IDS =
|
|
74
|
+
options.UNUSED_LENDING_POOL_IDS.length > 0
|
|
75
|
+
? options.UNUSED_LENDING_POOL_IDS
|
|
76
|
+
: [''];
|
|
59
77
|
this.isLiteDeployment = options.isLiteDeployment ?? false;
|
|
60
78
|
this.poolMetadataMapping = options.poolMetadataMapping ?? {};
|
|
61
79
|
this.stableAssetDecimals = options.stableAssetDecimals ?? 6;
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
KasuPoolExternalTVLAbi,
|
|
18
18
|
KasuPoolExternalTVLAbi__factory,
|
|
19
19
|
} from '../../contracts';
|
|
20
|
+
import { epochRateToApy } from '../../domain/rates';
|
|
20
21
|
import { SdkConfig } from '../../sdk-config';
|
|
21
22
|
import { getSystemVariablesQuery } from '../Locking/queries';
|
|
22
23
|
import { SystemVariables } from '../Locking/types';
|
|
@@ -171,9 +172,13 @@ export class DataService {
|
|
|
171
172
|
);
|
|
172
173
|
}
|
|
173
174
|
|
|
175
|
+
/**
|
|
176
|
+
* The subgraph's per-epoch `interestRate` compounded into an APY.
|
|
177
|
+
* Unchanged arithmetic — `EPOCHS_IN_YEAR` and the formula now come from
|
|
178
|
+
* `domain/rates` so this file and `KSULocking` cannot drift apart.
|
|
179
|
+
*/
|
|
174
180
|
calculateApyForTranche(interestRate: string): number {
|
|
175
|
-
|
|
176
|
-
return (1 + parseFloat(interestRate)) ** EPOCHS_IN_YEAR - 1;
|
|
181
|
+
return epochRateToApy(parseFloat(interestRate));
|
|
177
182
|
}
|
|
178
183
|
|
|
179
184
|
async getPlatformOverview(): Promise<PlatformOverviewDirectus> {
|
|
@@ -0,0 +1,52 @@
|
|
|
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
|
+
});
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
IUserManagerAbi,
|
|
18
18
|
IUserManagerAbi__factory,
|
|
19
19
|
} from '../../contracts';
|
|
20
|
+
import { epochRateToApy } from '../../domain/rates';
|
|
20
21
|
import { SdkConfig } from '../../sdk-config';
|
|
21
22
|
import {
|
|
22
23
|
getAllTrancheConfigurationsQuery,
|
|
@@ -265,9 +266,22 @@ export class KSULocking {
|
|
|
265
266
|
);
|
|
266
267
|
}
|
|
267
268
|
|
|
269
|
+
/**
|
|
270
|
+
* A per-epoch interest rate compounded into an APY.
|
|
271
|
+
*
|
|
272
|
+
* This used to read `(1 + epochInterestRate) ^ (EPOCHS_IN_YEAR - 1)`.
|
|
273
|
+
* `^` is JavaScript's BITWISE XOR, not exponentiation: both operands were
|
|
274
|
+
* coerced to int32, so for a realistic epoch rate of ~0.003 it computed
|
|
275
|
+
* `1 ^ 51` and returned the constant 50 — a 5000% APY — for every tranche,
|
|
276
|
+
* regardless of its rate. Its only caller,
|
|
277
|
+
* `calculateUserLockProjectedProtocolFeeRewards`, multiplied that by each
|
|
278
|
+
* tranche balance, so its projection was nonsense rather than merely
|
|
279
|
+
* imprecise.
|
|
280
|
+
*
|
|
281
|
+
* Now the same compounding every other rate in the SDK uses.
|
|
282
|
+
*/
|
|
268
283
|
calculateApy(epochInterestRate: number): number {
|
|
269
|
-
|
|
270
|
-
return (1 + epochInterestRate) ^ (EPOCHS_IN_YEAR - 1);
|
|
284
|
+
return epochRateToApy(epochInterestRate);
|
|
271
285
|
}
|
|
272
286
|
|
|
273
287
|
async getUserLocks(userAddress: string): Promise<UserLock[]> {
|