@kasufinance/kasu-sdk 2.4.2 → 2.6.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.
Files changed (180) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +201 -253
  3. package/dist/bundle.cjs.js +5472 -1710
  4. package/dist/bundle.esm.js +5415 -1713
  5. package/dist/domain/au-minimum.d.ts +135 -0
  6. package/dist/domain/au-minimum.js +154 -0
  7. package/dist/domain/au-minimum.js.map +1 -0
  8. package/dist/domain/au-minimum.test.js +202 -0
  9. package/dist/domain/au-minimum.test.js.map +1 -0
  10. package/dist/domain/deposit-bounds.d.ts +62 -0
  11. package/dist/domain/deposit-bounds.js +84 -0
  12. package/dist/domain/deposit-bounds.js.map +1 -0
  13. package/dist/domain/deposit-bounds.test.d.ts +1 -0
  14. package/dist/domain/deposit-bounds.test.js +135 -0
  15. package/dist/domain/deposit-bounds.test.js.map +1 -0
  16. package/dist/domain/index.d.ts +40 -0
  17. package/dist/domain/index.js +33 -0
  18. package/dist/domain/index.js.map +1 -0
  19. package/dist/domain/loan-contract.d.ts +174 -0
  20. package/dist/domain/loan-contract.js +160 -0
  21. package/dist/domain/loan-contract.js.map +1 -0
  22. package/dist/domain/loan-contract.test.d.ts +1 -0
  23. package/dist/domain/loan-contract.test.js +255 -0
  24. package/dist/domain/loan-contract.test.js.map +1 -0
  25. package/dist/domain/partners.d.ts +34 -0
  26. package/dist/domain/partners.js +43 -0
  27. package/dist/domain/partners.js.map +1 -0
  28. package/dist/domain/partners.test.d.ts +1 -0
  29. package/dist/domain/partners.test.js +53 -0
  30. package/dist/domain/partners.test.js.map +1 -0
  31. package/dist/domain/pools.d.ts +70 -0
  32. package/dist/domain/pools.js +134 -0
  33. package/dist/domain/pools.js.map +1 -0
  34. package/dist/domain/pools.test.d.ts +1 -0
  35. package/dist/domain/pools.test.js +184 -0
  36. package/dist/domain/pools.test.js.map +1 -0
  37. package/dist/domain/rates.d.ts +83 -0
  38. package/dist/domain/rates.js +120 -0
  39. package/dist/domain/rates.js.map +1 -0
  40. package/dist/domain/rates.test.d.ts +1 -0
  41. package/dist/domain/rates.test.js +181 -0
  42. package/dist/domain/rates.test.js.map +1 -0
  43. package/dist/domain/requests.d.ts +181 -0
  44. package/dist/domain/requests.js +202 -0
  45. package/dist/domain/requests.js.map +1 -0
  46. package/dist/domain/requests.test.d.ts +1 -0
  47. package/dist/domain/requests.test.js +470 -0
  48. package/dist/domain/requests.test.js.map +1 -0
  49. package/dist/domain/settlement.d.ts +97 -0
  50. package/dist/domain/settlement.js +117 -0
  51. package/dist/domain/settlement.js.map +1 -0
  52. package/dist/domain/settlement.test.d.ts +1 -0
  53. package/dist/domain/settlement.test.js +152 -0
  54. package/dist/domain/settlement.test.js.map +1 -0
  55. package/dist/domain/tranche-display-name.d.ts +17 -0
  56. package/dist/domain/tranche-display-name.js +30 -0
  57. package/dist/domain/tranche-display-name.js.map +1 -0
  58. package/dist/domain/tranche-display-name.test.d.ts +1 -0
  59. package/dist/domain/tranche-display-name.test.js +58 -0
  60. package/dist/domain/tranche-display-name.test.js.map +1 -0
  61. package/dist/domain/tranches.d.ts +89 -0
  62. package/dist/domain/tranches.js +150 -0
  63. package/dist/domain/tranches.js.map +1 -0
  64. package/dist/domain/tranches.test.d.ts +1 -0
  65. package/dist/domain/tranches.test.js +206 -0
  66. package/dist/domain/tranches.test.js.map +1 -0
  67. package/dist/domain/wallet-errors.d.ts +37 -0
  68. package/dist/domain/wallet-errors.js +56 -0
  69. package/dist/domain/wallet-errors.js.map +1 -0
  70. package/dist/domain/wallet-errors.test.d.ts +1 -0
  71. package/dist/domain/wallet-errors.test.js +71 -0
  72. package/dist/domain/wallet-errors.test.js.map +1 -0
  73. package/dist/facade/chain-configs.js +61 -1
  74. package/dist/facade/chain-configs.js.map +1 -1
  75. package/dist/facade/config.test.d.ts +1 -0
  76. package/dist/facade/config.test.js +216 -0
  77. package/dist/facade/config.test.js.map +1 -0
  78. package/dist/facade/deposits.d.ts +22 -1
  79. package/dist/facade/deposits.js +24 -1
  80. package/dist/facade/deposits.js.map +1 -1
  81. package/dist/facade/facade.test.d.ts +1 -0
  82. package/dist/{tests → facade}/facade.test.js +87 -5
  83. package/dist/facade/facade.test.js.map +1 -0
  84. package/dist/facade/index.d.ts +2 -1
  85. package/dist/facade/index.js +2 -0
  86. package/dist/facade/index.js.map +1 -1
  87. package/dist/facade/kasu.d.ts +49 -8
  88. package/dist/facade/kasu.js +117 -13
  89. package/dist/facade/kasu.js.map +1 -1
  90. package/dist/facade/strategies.d.ts +25 -0
  91. package/dist/facade/strategies.js +36 -0
  92. package/dist/facade/strategies.js.map +1 -1
  93. package/dist/facade/types.d.ts +57 -2
  94. package/dist/facade/unused-pool-ids.d.ts +31 -0
  95. package/dist/facade/unused-pool-ids.js +57 -0
  96. package/dist/facade/unused-pool-ids.js.map +1 -0
  97. package/dist/facade/user-portfolio.d.ts +18 -0
  98. package/dist/facade/user-portfolio.js +23 -0
  99. package/dist/facade/user-portfolio.js.map +1 -1
  100. package/dist/index.d.ts +2 -0
  101. package/dist/index.js +6 -0
  102. package/dist/index.js.map +1 -1
  103. package/dist/sdk-config.d.ts +11 -0
  104. package/dist/sdk-config.js +8 -1
  105. package/dist/sdk-config.js.map +1 -1
  106. package/dist/services/DataService/data-service.d.ts +5 -0
  107. package/dist/services/DataService/data-service.js +10 -12
  108. package/dist/services/DataService/data-service.js.map +1 -1
  109. package/dist/services/DataService/directus-client.d.ts +26 -0
  110. package/dist/services/DataService/directus-client.js +38 -0
  111. package/dist/services/DataService/directus-client.js.map +1 -0
  112. package/dist/services/Locking/calculate-apy.test.d.ts +1 -0
  113. package/dist/services/Locking/calculate-apy.test.js +41 -0
  114. package/dist/services/Locking/calculate-apy.test.js.map +1 -0
  115. package/dist/services/Locking/locking.d.ts +14 -0
  116. package/dist/services/Locking/locking.js +16 -2
  117. package/dist/services/Locking/locking.js.map +1 -1
  118. package/dist/services/UserLending/user-lending.js +11 -7
  119. package/dist/services/UserLending/user-lending.js.map +1 -1
  120. package/dist/tests/sample.test.js +46 -25
  121. package/dist/tests/sample.test.js.map +1 -1
  122. package/package.json +43 -12
  123. package/src/domain/au-minimum.test.ts +371 -0
  124. package/src/domain/au-minimum.ts +192 -0
  125. package/src/domain/deposit-bounds.test.ts +200 -0
  126. package/src/domain/deposit-bounds.ts +118 -0
  127. package/src/domain/index.ts +134 -0
  128. package/src/domain/loan-contract.test.ts +343 -0
  129. package/src/domain/loan-contract.ts +275 -0
  130. package/src/domain/partners.test.ts +83 -0
  131. package/src/domain/partners.ts +66 -0
  132. package/src/domain/pools.test.ts +260 -0
  133. package/src/domain/pools.ts +150 -0
  134. package/src/domain/rates.test.ts +254 -0
  135. package/src/domain/rates.ts +120 -0
  136. package/src/domain/requests.test.ts +653 -0
  137. package/src/domain/requests.ts +414 -0
  138. package/src/domain/settlement.test.ts +198 -0
  139. package/src/domain/settlement.ts +161 -0
  140. package/src/domain/tranche-display-name.test.ts +96 -0
  141. package/src/domain/tranche-display-name.ts +34 -0
  142. package/src/domain/tranches.test.ts +300 -0
  143. package/src/domain/tranches.ts +177 -0
  144. package/src/domain/wallet-errors.test.ts +100 -0
  145. package/src/domain/wallet-errors.ts +56 -0
  146. package/src/facade/chain-configs.ts +62 -1
  147. package/src/facade/config.test.ts +265 -0
  148. package/src/facade/deposits.ts +25 -0
  149. package/src/{tests → facade}/facade.test.ts +134 -5
  150. package/src/facade/index.ts +4 -0
  151. package/src/facade/kasu.ts +155 -12
  152. package/src/facade/strategies.ts +36 -0
  153. package/src/facade/types.ts +60 -2
  154. package/src/facade/unused-pool-ids.ts +59 -0
  155. package/src/facade/user-portfolio.ts +24 -0
  156. package/src/index.ts +8 -0
  157. package/src/sdk-config.ts +19 -1
  158. package/src/services/DataService/data-service.ts +14 -26
  159. package/src/services/DataService/directus-client.ts +54 -0
  160. package/src/services/Locking/calculate-apy.test.ts +52 -0
  161. package/src/services/Locking/locking.ts +16 -2
  162. package/src/services/UserLending/user-lending.ts +17 -21
  163. package/src/tests/sample.test.ts +54 -33
  164. package/.eslintignore +0 -3
  165. package/.eslintrc.json +0 -90
  166. package/.github/workflows/ci.yml +0 -39
  167. package/.github/workflows/gitleaks.yml +0 -22
  168. package/.github/workflows/main.yml +0 -41
  169. package/.gitleaksignore +0 -19
  170. package/.prettierignore +0 -3
  171. package/.prettierrc +0 -28
  172. package/CLAUDE.md +0 -410
  173. package/copy-abi.ps1 +0 -15
  174. package/dist/tests/facade.test.js.map +0 -1
  175. package/index.ts +0 -2
  176. package/jest-config.ts +0 -9
  177. package/jest.config.js +0 -8
  178. package/rollup.config.js +0 -45
  179. package/tsconfig.json +0 -27
  180. /package/dist/{tests/facade.test.d.ts → domain/au-minimum.test.d.ts} +0 -0
@@ -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 { ChainConfigEntry, KasuOptions, SupportedChain } from './types';
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
- * const kasu = Kasu.create({ chain: 'base', signerOrProvider: provider });
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
- * // Deposit
25
- * const tx = await kasu.deposits.deposit({ poolId, trancheId, amount, kycSignature });
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 — uses built-in Base mainnet config
71
- * const kasu = Kasu.create({ chain: 'base', signerOrProvider: provider });
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 sdk = new KasuSdk(sdkConfig, options.signerOrProvider);
108
- return new Kasu(sdk, chainConfig);
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 {
@@ -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
  */
@@ -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
- /** ethers Signer (for transactions) or Provider (read-only). */
32
- signerOrProvider: import('ethers').Signer | import('@ethersproject/providers').Provider;
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
+ }
@@ -1,5 +1,6 @@
1
1
  import { Provider } from '@ethersproject/providers';
2
2
 
3
+ import { deriveRequestState, RequestState } from '../domain/requests';
3
4
  import { DataService } from '../services/DataService/data-service';
4
5
  import { Portfolio } from '../services/Portfolio/portfolio';
5
6
  import { UserRequest } from '../services/UserLending/types';
@@ -70,4 +71,27 @@ export class PortfolioFacade {
70
71
  currentEpoch,
71
72
  );
72
73
  }
74
+
75
+ /**
76
+ * The same history, already derived into `RequestState` rows — status
77
+ * code, kind, signed amount, the cancelled-amount recovery, the bundled
78
+ * submission count and the cycle-open signal.
79
+ *
80
+ * ```ts
81
+ * const rows = await kasu.portfolio.getRequestStates('0xUser...');
82
+ * rows.filter((r) => r.statusCode === 'pending');
83
+ * ```
84
+ *
85
+ * Every application derives this from `getTransactionHistory` anyway, and
86
+ * the derivation is the part they were each getting subtly differently.
87
+ * Words are still the caller's: render `statusCode` in your own
88
+ * vocabulary, and call `getTrancheDisplayName` on `trancheName` at the
89
+ * view boundary.
90
+ */
91
+ async getRequestStates(
92
+ userAddress: `0x${string}`,
93
+ ): Promise<RequestState[]> {
94
+ const requests = await this.getTransactionHistory(userAddress);
95
+ return requests.map(deriveRequestState);
96
+ }
73
97
  }
package/src/index.ts CHANGED
@@ -55,7 +55,15 @@ export type {
55
55
  PlatformOverviewDirectus,
56
56
  } from './services/DataService/directus-types';
57
57
 
58
+ export { NO_DIRECTUS_URL_MESSAGE } from './services/DataService/directus-client';
59
+
58
60
  // ---------------------------------------------------------------------------
59
61
  // Re-export facade (high-level integrator API)
60
62
  // ---------------------------------------------------------------------------
61
63
  export * from './facade';
64
+
65
+ // ---------------------------------------------------------------------------
66
+ // Shared domain layer — pure rate, tranche and pool rules. Numbers and codes
67
+ // only: no copy, no locale. See `src/domain/index.ts`.
68
+ // ---------------------------------------------------------------------------
69
+ 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
- this.UNUSED_LENDING_POOL_IDS = options.UNUSED_LENDING_POOL_IDS;
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;
@@ -1,13 +1,4 @@
1
- import {
2
- authentication,
3
- AuthenticationClient,
4
- createDirectus,
5
- DirectusClient,
6
- readItems,
7
- readSingleton,
8
- rest,
9
- RestClient,
10
- } from '@directus/sdk';
1
+ import { readItems, readSingleton } from '@directus/sdk';
11
2
  import { Provider } from '@ethersproject/abstract-provider';
12
3
  import { Signer } from 'ethers';
13
4
  import { formatUnits } from 'ethers/lib/utils';
@@ -17,14 +8,18 @@ import {
17
8
  KasuPoolExternalTVLAbi,
18
9
  KasuPoolExternalTVLAbi__factory,
19
10
  } from '../../contracts';
11
+ import { epochRateToApy } from '../../domain/rates';
20
12
  import { SdkConfig } from '../../sdk-config';
21
13
  import { getSystemVariablesQuery } from '../Locking/queries';
22
14
  import { SystemVariables } from '../Locking/types';
23
15
  import { filterArray } from '../shared';
24
16
 
17
+ import {
18
+ createDirectusClient,
19
+ KasuDirectusClient,
20
+ } from './directus-client';
25
21
  import {
26
22
  BadAndDoubtfulDebtsItems,
27
- DirectusSchema,
28
23
  FinancialReportingDocumentsItemsDirectus,
29
24
  KeyCreditMetricsDirectus,
30
25
  NftBoostDirectus,
@@ -66,9 +61,7 @@ import {
66
61
  export class DataService {
67
62
  private readonly _graph: GraphQLClient;
68
63
  private readonly _externalTvlAbi: KasuPoolExternalTVLAbi;
69
- private readonly _directus: DirectusClient<DirectusSchema> &
70
- AuthenticationClient<DirectusSchema> &
71
- RestClient<DirectusSchema>;
64
+ private readonly _directus: KasuDirectusClient;
72
65
  private _directusPoolOverview: PoolOverviewDirectus[] | undefined;
73
66
 
74
67
  constructor(
@@ -81,16 +74,7 @@ export class DataService {
81
74
  );
82
75
  this._graph = new GraphQLClient(_kasuConfig.subgraphUrl);
83
76
 
84
- if (_kasuConfig.directusUrl) {
85
- this._directus = createDirectus<DirectusSchema>(
86
- _kasuConfig.directusUrl,
87
- )
88
- .with(authentication())
89
- .with(rest());
90
- } else {
91
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
92
- this._directus = null as any;
93
- }
77
+ this._directus = createDirectusClient(_kasuConfig.directusUrl);
94
78
  }
95
79
 
96
80
  private getUrlFromFile(fileName: string): string {
@@ -171,9 +155,13 @@ export class DataService {
171
155
  );
172
156
  }
173
157
 
158
+ /**
159
+ * The subgraph's per-epoch `interestRate` compounded into an APY.
160
+ * Unchanged arithmetic — `EPOCHS_IN_YEAR` and the formula now come from
161
+ * `domain/rates` so this file and `KSULocking` cannot drift apart.
162
+ */
174
163
  calculateApyForTranche(interestRate: string): number {
175
- const EPOCHS_IN_YEAR = 52.17857;
176
- return (1 + parseFloat(interestRate)) ** EPOCHS_IN_YEAR - 1;
164
+ return epochRateToApy(parseFloat(interestRate));
177
165
  }
178
166
 
179
167
  async getPlatformOverview(): Promise<PlatformOverviewDirectus> {
@@ -0,0 +1,54 @@
1
+ import {
2
+ authentication,
3
+ AuthenticationClient,
4
+ createDirectus,
5
+ DirectusClient,
6
+ rest,
7
+ RestClient,
8
+ } from '@directus/sdk';
9
+
10
+ import { DirectusSchema } from './directus-types';
11
+
12
+ /** The Directus client shape both services hold. */
13
+ export type KasuDirectusClient = DirectusClient<DirectusSchema> &
14
+ AuthenticationClient<DirectusSchema> &
15
+ RestClient<DirectusSchema>;
16
+
17
+ /**
18
+ * The error a CMS-only call raises on a deployment configured without
19
+ * Directus. Named so a caller can match on it rather than on the message.
20
+ */
21
+ export const NO_DIRECTUS_URL_MESSAGE =
22
+ 'Kasu: this call needs Directus, but the SDK was configured without a ' +
23
+ '`directusUrl`. On-chain data (pools, tranches, positions, requests) ' +
24
+ 'works without one; CMS content does not.';
25
+
26
+ /**
27
+ * Build the Directus client, or a stand-in that refuses clearly.
28
+ *
29
+ * `directusUrl` is documented optional, and most of the SDK genuinely does not
30
+ * need it — pools, tranches, positions and request history all come from the
31
+ * subgraph and the chain. But `createDirectus('')` throws `Invalid URL` inside
32
+ * the constructor, so omitting the URL used to make the whole SDK
33
+ * unconstructable rather than merely CMS-less.
34
+ *
35
+ * With no URL, the services skip Directus where they can degrade (pool
36
+ * descriptions, images and Directus pool names simply do not appear, and the
37
+ * raw subgraph names are used instead), and a call that exists ONLY to read
38
+ * CMS content rejects with `NO_DIRECTUS_URL_MESSAGE` — a sentence that says
39
+ * what to configure, rather than a `null` dereference thrown from inside a
40
+ * vendor SDK.
41
+ */
42
+ export function createDirectusClient(directusUrl: string): KasuDirectusClient {
43
+ if (directusUrl) {
44
+ return createDirectus<DirectusSchema>(directusUrl)
45
+ .with(authentication())
46
+ .with(rest());
47
+ }
48
+ const refuse = (): never => {
49
+ throw new Error(NO_DIRECTUS_URL_MESSAGE);
50
+ };
51
+ // A stand-in, not a client: every entry point the services use goes
52
+ // through `request`, so refusing there covers all of them.
53
+ return { request: refuse } as unknown as KasuDirectusClient;
54
+ }