@kasufinance/kasu-sdk 2.14.0 → 2.15.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 (46) hide show
  1. package/README.md +29 -5
  2. package/dist/bundle.cjs.js +561 -11
  3. package/dist/bundle.esm.js +553 -12
  4. package/dist/domain/index.d.ts +6 -1
  5. package/dist/domain/index.js +6 -2
  6. package/dist/domain/index.js.map +1 -1
  7. package/dist/domain/loan-contract.d.ts +16 -5
  8. package/dist/domain/loan-contract.js +16 -5
  9. package/dist/domain/loan-contract.js.map +1 -1
  10. package/dist/domain/pool-tvl.js +179 -0
  11. package/dist/domain/pool-tvl.js.map +1 -1
  12. package/dist/domain/requests.js +29 -19
  13. package/dist/domain/requests.js.map +1 -1
  14. package/dist/domain/strategy-details.d.ts +262 -0
  15. package/dist/domain/strategy-details.js +375 -0
  16. package/dist/domain/strategy-details.js.map +1 -0
  17. package/dist/facade/index.d.ts +1 -1
  18. package/dist/facade/index.js +2 -0
  19. package/dist/facade/index.js.map +1 -1
  20. package/dist/facade/strategies.d.ts +85 -3
  21. package/dist/facade/strategies.js +182 -6
  22. package/dist/facade/strategies.js.map +1 -1
  23. package/dist/facade/strategy-deployments.js +97 -0
  24. package/dist/facade/strategy-deployments.js.map +1 -0
  25. package/dist/facade/types.d.ts +82 -0
  26. package/dist/flows/deposit-flow.d.ts +19 -5
  27. package/dist/flows/deposit-flow.js.map +1 -1
  28. package/dist/index.js.map +1 -1
  29. package/dist/services/DataService/data-service.js +80 -9
  30. package/dist/services/DataService/data-service.js.map +1 -1
  31. package/dist/services/DataService/tvl-history.js +89 -0
  32. package/dist/services/DataService/tvl-history.js.map +1 -1
  33. package/dist/services/UserLending/queries.js +2 -7
  34. package/dist/services/UserLending/queries.js.map +1 -1
  35. package/dist/services/UserLending/subgraph-types.js.map +1 -1
  36. package/dist/services/UserLending/types.js.map +1 -1
  37. package/dist/services/UserLending/user-lending.js +72 -16
  38. package/dist/services/UserLending/user-lending.js.map +1 -1
  39. package/package.json +1 -1
  40. package/src/domain/index.ts +27 -1
  41. package/src/domain/loan-contract.ts +16 -5
  42. package/src/domain/strategy-details.ts +528 -0
  43. package/src/facade/index.ts +3 -0
  44. package/src/facade/strategies.ts +205 -10
  45. package/src/facade/types.ts +85 -0
  46. package/src/flows/deposit-flow.ts +19 -5
@@ -1,5 +1,14 @@
1
1
  import { PoolTvlHistoryEntry } from '../domain/pool-tvl';
2
2
  import { selectVisiblePools } from '../domain/pools';
3
+ import {
4
+ finiteOrUndefined,
5
+ groupRiskManagementItems,
6
+ parseFacility,
7
+ readCmsStartDate,
8
+ securityStructureOf,
9
+ splitBusinessTypes,
10
+ StrategyRiskGroup,
11
+ } from '../domain/strategy-details';
3
12
  import { pickShortDescription } from '../domain/strategy-summary';
4
13
  import { DataService } from '../services/DataService/data-service';
5
14
  import {
@@ -15,8 +24,11 @@ import { UserLending } from '../services/UserLending/user-lending';
15
24
  import {
16
25
  DepositLimits,
17
26
  FixedTermOption,
27
+ OriginatorHistory,
18
28
  PlatformStats,
19
29
  Strategy,
30
+ StrategyEndBorrowers,
31
+ StrategyHistory,
20
32
  StrategyRiskPerformance,
21
33
  StrategyTranche,
22
34
  } from './types';
@@ -35,8 +47,65 @@ export class StrategiesFacade {
35
47
  * share their Base counterpart's editorial record). Lowercase keys.
36
48
  */
37
49
  private _poolMetadataMapping: Record<string, string> = {},
50
+ /**
51
+ * The clock the CMS start dates are checked against (a date after it
52
+ * is refused). Injected for tests; the current time otherwise.
53
+ */
54
+ private _now: () => Date = (): Date => new Date(),
55
+ /**
56
+ * Milliseconds on a clock that only moves forward, for the shared
57
+ * CMS reads' 30-second lifetime — separate from `_now`, so a pinned
58
+ * date clock cannot keep a read alive for ever. Injected for tests;
59
+ * `Date.now` otherwise.
60
+ */
61
+ private _memoClockMs: () => number = (): number => Date.now(),
38
62
  ) {}
39
63
 
64
+ /**
65
+ * Short-lived, per-instance memo of the two CMS-wide reads the pool
66
+ * getters share, so a Details screen asking for the loss rate, the risk
67
+ * groups and the originator together reads each collection once. Keyed
68
+ * by read; holds the in-flight promise, so concurrent callers share it.
69
+ * A REJECTED read is dropped at once, never served again — the next call
70
+ * retries.
71
+ */
72
+ private _cmsReads = new Map<
73
+ string,
74
+ { promise: Promise<unknown>; expiresAt: number }
75
+ >();
76
+
77
+ private _memoCmsRead<T>(key: string, read: () => Promise<T>): Promise<T> {
78
+ const nowMs = this._memoClockMs();
79
+ const cached = this._cmsReads.get(key);
80
+ if (cached && cached.expiresAt > nowMs) {
81
+ return cached.promise as Promise<T>;
82
+ }
83
+ const promise = read();
84
+ const entry = { promise, expiresAt: nowMs + CMS_READ_TTL_MS };
85
+ this._cmsReads.set(key, entry);
86
+ promise.catch(() => {
87
+ // Only our own entry: a newer read may have replaced it.
88
+ if (this._cmsReads.get(key) === entry) this._cmsReads.delete(key);
89
+ });
90
+ return promise;
91
+ }
92
+
93
+ private _riskManagementRecords(): ReturnType<
94
+ DataService['getRiskManagement']
95
+ > {
96
+ return this._memoCmsRead('riskManagement', () =>
97
+ this._dataService.getRiskManagement(),
98
+ );
99
+ }
100
+
101
+ private _originatorProfiles(): ReturnType<
102
+ DataService['getPoolDelegateProfileAndHistory']
103
+ > {
104
+ return this._memoCmsRead('originatorProfiles', () =>
105
+ this._dataService.getPoolDelegateProfileAndHistory(),
106
+ );
107
+ }
108
+
40
109
  /**
41
110
  * The pool's loss rate and first-loss capital, from its CMS
42
111
  * risk-management record. `null` when the pool has no record.
@@ -48,13 +117,15 @@ export class StrategiesFacade {
48
117
  * 0..1 fraction.
49
118
  *
50
119
  * Requires `directusUrl`; without it the call rejects with
51
- * `NO_DIRECTUS_URL_MESSAGE`, like every CMS-only read.
120
+ * `NO_DIRECTUS_URL_MESSAGE`, like every CMS-only read. Shares its CMS
121
+ * read with `getRiskManagementGroups` for 30 seconds per instance; a
122
+ * failed read is never reused.
52
123
  */
53
124
  async getRiskPerformance(
54
125
  poolId: string,
55
126
  ): Promise<StrategyRiskPerformance | null> {
56
127
  const metadataId = this._metadataIdOf(poolId);
57
- const records = await this._dataService.getRiskManagement();
128
+ const records = await this._riskManagementRecords();
58
129
  const record = records.find(
59
130
  (r) => String(r.poolIdFK).toLowerCase() === metadataId,
60
131
  );
@@ -82,6 +153,10 @@ export class StrategiesFacade {
82
153
  * deployment every profile lists no pools and this resolves `null` —
83
154
  * the same answer kasu-ui's own lookup gives there today. The metadata
84
155
  * candidate becomes live once the service resolves the mapping.
156
+ *
157
+ * Shares its reads with `getOriginatorHistory` for 30 seconds per
158
+ * instance; a failed read is never reused. Each call returns its own
159
+ * copy of the profile, so mutating it changes nothing shared.
85
160
  */
86
161
  async getOriginator(
87
162
  poolId: string,
@@ -90,15 +165,102 @@ export class StrategiesFacade {
90
165
  poolId.toLowerCase(),
91
166
  this._metadataIdOf(poolId),
92
167
  ]);
93
- const profiles =
94
- await this._dataService.getPoolDelegateProfileAndHistory();
95
- return (
96
- profiles.find((profile) =>
97
- profile.otherKASUPools.some((pool) =>
98
- candidates.has(String(pool.id).toLowerCase()),
99
- ),
100
- ) ?? null
168
+ const profiles = await this._originatorProfiles();
169
+ const profile = profiles.find((candidate) =>
170
+ candidate.otherKASUPools.some((pool) =>
171
+ candidates.has(String(pool.id).toLowerCase()),
172
+ ),
173
+ );
174
+ // A copy: the profiles are shared between calls (see
175
+ // `_memoCmsRead`), so a caller sorting or editing its result must
176
+ // not change what the next call returns.
177
+ return profile
178
+ ? {
179
+ ...profile,
180
+ otherKASUPools: profile.otherKASUPools.map((pool) => ({
181
+ ...pool,
182
+ })),
183
+ }
184
+ : null;
185
+ }
186
+
187
+ /**
188
+ * The credit originator's ENTIRE lending history for a pool — since when,
189
+ * total originated, loans outstanding, loss rate — read from the profile
190
+ * `getOriginator` finds, each figure validated. `null` when no profile
191
+ * lists the pool, or the profile authors none of the four.
192
+ *
193
+ * kasu-ui's Details tab shows these as its Credit Originator strip, and
194
+ * hides the strip when there is no profile. The money figures are
195
+ * editorial USD on every deployment: never FX-convert them. An XDC pool
196
+ * resolves `null` today, as it does in kasu-ui (see `getOriginator`).
197
+ *
198
+ * Requires `directusUrl`, like every CMS-only read; rejects when the
199
+ * read fails. Shares its reads with `getOriginator` for 30 seconds per
200
+ * instance; a failed read is never reused.
201
+ */
202
+ async getOriginatorHistory(
203
+ poolId: string,
204
+ ): Promise<OriginatorHistory | null> {
205
+ const profile = await this.getOriginator(poolId);
206
+ if (!profile) return null;
207
+ const history: OriginatorHistory = {};
208
+ const since = readCmsStartDate(
209
+ profile.delegateLendingHistory,
210
+ this._now(),
211
+ { unixSeconds: true },
212
+ );
213
+ if (since !== undefined) history.since = since;
214
+ const totalOriginated = finiteOrUndefined(
215
+ profile.totalLoanFundsOriginated,
216
+ );
217
+ if (totalOriginated !== undefined) {
218
+ history.totalOriginated = totalOriginated;
219
+ }
220
+ const loansOutstanding = finiteOrUndefined(
221
+ profile.loansUnderManagement,
222
+ );
223
+ if (loansOutstanding !== undefined) {
224
+ history.loansOutstanding = loansOutstanding;
225
+ }
226
+ const lossRate = finiteOrUndefined(profile.historicLossRate);
227
+ if (lossRate !== undefined) history.lossRate = lossRate;
228
+ return Object.keys(history).length > 0 ? history : null;
229
+ }
230
+
231
+ /**
232
+ * The pool's Risk Management groups, from its CMS risk-management record:
233
+ * each group with its items (name, the pool's description of it split
234
+ * into lines, the shared tooltip), the groups in `RISK_GROUP_ORDER`
235
+ * (Security & Recourse, Structural Controls, Financial Covenants,
236
+ * Reporting Covenants, Key Eligibility Criteria, then any other in CMS
237
+ * order). See `groupRiskManagementItems`.
238
+ *
239
+ * `null` when the pool has no record, or the record has no usable item —
240
+ * then kasu-ui falls back to `Strategy.securityStructure`. XDC pools
241
+ * resolve through the metadata mapping to their Base record, like
242
+ * `getRiskPerformance`.
243
+ *
244
+ * REJECTS when the CMS read fails (an outage, or no `directusUrl` —
245
+ * `NO_DIRECTUS_URL_MESSAGE`), so a failure stays visible rather than
246
+ * reading as "no items". For what to SHOW, treat a rejection like `null`:
247
+ * fall back to `Strategy.securityStructure`, which arrives with the
248
+ * strategy and needs no second read.
249
+ *
250
+ * Shares its CMS read with `getRiskPerformance` for 30 seconds per
251
+ * instance; a failed read is never reused.
252
+ */
253
+ async getRiskManagementGroups(
254
+ poolId: string,
255
+ ): Promise<StrategyRiskGroup[] | null> {
256
+ const metadataId = this._metadataIdOf(poolId);
257
+ const records = await this._riskManagementRecords();
258
+ const record = records.find(
259
+ (r) => String(r.poolIdFK).toLowerCase() === metadataId,
101
260
  );
261
+ if (!record || !Array.isArray(record.items)) return null;
262
+ const groups = groupRiskManagementItems(record.items);
263
+ return groups.length > 0 ? groups : null;
102
264
  }
103
265
 
104
266
  private _metadataIdOf(poolId: string): string {
@@ -281,11 +443,44 @@ export class StrategiesFacade {
281
443
  apyStructure: pool.poolApyStructure,
282
444
  thumbnailUrl: pool.thumbnailImageUrl,
283
445
  bannerUrl: pool.bannerImageUrl,
446
+ strategyHistory: strategyHistoryOf(pool, this._now()),
447
+ endBorrowers: endBorrowersOf(pool),
448
+ securityStructure: securityStructureOf(pool.security),
284
449
  _raw: pool,
285
450
  };
286
451
  }
287
452
  }
288
453
 
454
+ /**
455
+ * How long a CMS-wide read is shared between the pool getters. Long enough
456
+ * for one screen's worth of calls, short enough that a CMS edit shows on the
457
+ * next visit; apps cache further on their own terms.
458
+ */
459
+ const CMS_READ_TTL_MS = 30_000;
460
+
461
+ /** `Strategy.strategyHistory`: an absent figure is an absent key. */
462
+ function strategyHistoryOf(pool: PoolOverview, now: Date): StrategyHistory {
463
+ const history: StrategyHistory = {};
464
+ const since = readCmsStartDate(pool.operatingSince, now);
465
+ if (since !== undefined) history.since = since;
466
+ const lenderInterestEarned = finiteOrUndefined(pool.yieldEarned);
467
+ if (lenderInterestEarned !== undefined) {
468
+ history.lenderInterestEarned = lenderInterestEarned;
469
+ }
470
+ return history;
471
+ }
472
+
473
+ /** Business Types and Facility Type, or `undefined` when neither is authored. */
474
+ function endBorrowersOf(pool: PoolOverview): StrategyEndBorrowers | undefined {
475
+ const businessTypes = splitBusinessTypes(pool.industryExposure);
476
+ const facility = parseFacility(pool.loanStructure);
477
+ if (!businessTypes && !facility) return undefined;
478
+ return {
479
+ ...(businessTypes ? { businessTypes } : {}),
480
+ ...(facility ? { facility } : {}),
481
+ };
482
+ }
483
+
289
484
  /**
290
485
  * A CMS text field as a string, or `undefined` when it is absent, not a
291
486
  * string, or empty (blank included) — the CMS stores an emptied field as `''`, and an empty
@@ -1,5 +1,6 @@
1
1
  import { BigNumberish, BytesLike } from 'ethers';
2
2
 
3
+ import { CmsTwoLineValue, StrategyFacility } from '../domain/strategy-details';
3
4
  import { ContractAddresses, SdkConfigOptions } from '../sdk-config';
4
5
  import {
5
6
  LendingTotals,
@@ -175,10 +176,94 @@ export interface Strategy {
175
176
  thumbnailUrl: string;
176
177
  /** Banner image URL (empty string when Directus unavailable). */
177
178
  bannerUrl: string;
179
+ /**
180
+ * The strategy's own lending history, as kasu-ui's Details tab states it
181
+ * beside Total Active Lending (`tvl.total`). Each figure is absent when
182
+ * unauthored or unreadable — omit its row, never print a stub.
183
+ *
184
+ * Always set by `getAll` / `getVisible` / `getById` (both figures inside
185
+ * may still be absent). Optional in the type only so that a `Strategy`
186
+ * built by hand against an earlier version still compiles.
187
+ */
188
+ strategyHistory?: StrategyHistory;
189
+ /**
190
+ * Who the credit originator lends to, and on what facility. Absent when
191
+ * the CMS authors neither.
192
+ */
193
+ endBorrowers?: StrategyEndBorrowers;
194
+ /**
195
+ * The CMS security tags (`security`), trimmed, blanks dropped. Absent when
196
+ * none are authored.
197
+ *
198
+ * kasu-ui shows these, under "Security Structure over End Borrowers", ONLY
199
+ * when `getRiskManagementGroups` resolves `null` — they are the fallback
200
+ * for a pool with no risk-management items, not a section of their own.
201
+ * Treat a REJECTED `getRiskManagementGroups` (a CMS outage) the same way
202
+ * for display: show these, and report the failure as you see fit.
203
+ */
204
+ securityStructure?: string[];
178
205
  /** Raw `PoolOverview` for power-users who need all fields. */
179
206
  _raw: PoolOverview;
180
207
  }
181
208
 
209
+ /** `Strategy.strategyHistory`. */
210
+ export interface StrategyHistory {
211
+ /**
212
+ * The date this strategy started lending (CMS `operatingSince`), as
213
+ * `YYYY-MM-DD`. Absent when unauthored, not an ISO date, before 2000 or
214
+ * in the future (see `readCmsStartDate`). Render it as elapsed time
215
+ * ("Strategy Lending History").
216
+ */
217
+ since?: string;
218
+ /**
219
+ * Interest earned by this pool's lenders since inception (the subgraph's
220
+ * `totalUserInterestAmount`, `PoolOverview.yieldEarned`), in display
221
+ * units of the deployment's OWN stable token — like `tvl.total`. For the
222
+ * strategy-wide figure across deployments in USD, see
223
+ * `strategyTotalsUsd`. Absent when the value is not a finite number.
224
+ */
225
+ lenderInterestEarned?: number;
226
+ }
227
+
228
+ /** `Strategy.endBorrowers`: at least one of the two is present. */
229
+ export interface StrategyEndBorrowers {
230
+ /**
231
+ * Business Types (CMS `industryExposure`): the value and its supporting
232
+ * clause. Absent when unauthored. See `splitBusinessTypes`.
233
+ */
234
+ businessTypes?: CmsTwoLineValue;
235
+ /**
236
+ * Facility Type (CMS `loanStructure`): the label, its clause, and whether
237
+ * it revolves. Absent when unauthored. See `parseFacility`.
238
+ */
239
+ facility?: StrategyFacility;
240
+ }
241
+
242
+ /**
243
+ * The credit originator's ENTIRE lending history — every loan it has made,
244
+ * not only this strategy's — from its CMS profile
245
+ * (`kasu.strategies.getOriginatorHistory`). Each figure is absent when
246
+ * unauthored or not a valid value; omit its tile, never print a stub.
247
+ *
248
+ * The money figures are editorial and already in USD, on every deployment:
249
+ * never convert them by the deployment's FX rate, and never sum them across
250
+ * a strategy's deployments.
251
+ */
252
+ export interface OriginatorHistory {
253
+ /**
254
+ * The date the originator started lending (CMS
255
+ * `delegateLendingHistory`), as `YYYY-MM-DD`. Render it as elapsed time
256
+ * ("Entire Lending History").
257
+ */
258
+ since?: string;
259
+ /** Total loan funds originated, USD (CMS `totalLoanFundsOriginated`). */
260
+ totalOriginated?: number;
261
+ /** Loans outstanding, USD (CMS `loansUnderManagement` on the profile). */
262
+ loansOutstanding?: number;
263
+ /** Historic loss rate, a 0..1 fraction (CMS `historicLossRate`). */
264
+ lossRate?: number;
265
+ }
266
+
182
267
  export interface StrategyTranche {
183
268
  id: string;
184
269
  name: string;
@@ -45,8 +45,9 @@ import { WaitableTransaction } from './observable';
45
45
  * lender is not in. A failed read assumes an approve IS needed: the safe
46
46
  * default is a redundant approval, never a reverted deposit.
47
47
  * 2. **`generating-sign`** — the lender signs the auth message
48
- * (`buildLoanAgreementSignMessage`, or the legacy builder). kasu-backend
49
- * reconstructs that string byte-for-byte to verify the signature.
48
+ * (`buildLoanAgreementSignMessage`, or the deprecated legacy builder).
49
+ * kasu-backend reconstructs that string byte-for-byte to verify the
50
+ * signature.
50
51
  * 3. **`generating-fetch`** — the agreements service returns the contract.
51
52
  * 4. **`awaiting-accept`** — the run PARKS on a promise the consumer settles
52
53
  * with `acceptContract()` or `declineContract()`. This is the only point at
@@ -266,10 +267,19 @@ export interface LoanAgreementRequest {
266
267
 
267
268
  /**
268
269
  * The legacy `I request contract content for {address} at {timestamp}.`
269
- * format, which kasu-backend still accepts and `/contract/resolve` has no
270
- * alternative to.
270
+ * format, which kasu-backend still accepts on `/contract/generate`. It is not
271
+ * the `/contract/resolve` message: resolve has its own, which the backend
272
+ * rebuilds and verifies.
273
+ *
274
+ * @deprecated Use `{ format: 'loan-agreement', … }` (`LoanAgreementRequest`).
275
+ * Only the retired kasu-fe-next app (retired 2026-11-05) and kasu-mobile
276
+ * builds from before 2026-09-06 used `format: 'legacy'`; it will be removed
277
+ * in the next major version. Behaviour is unchanged until then.
271
278
  */
272
279
  export interface LegacyContractRequest {
280
+ /**
281
+ * @deprecated Use `'loan-agreement'`. Removed in the next major version.
282
+ */
273
283
  format: 'legacy';
274
284
  }
275
285
 
@@ -378,7 +388,11 @@ export interface DepositFlowInput {
378
388
  * contract and then a revert diagnosed as `insufficient-balance`.
379
389
  */
380
390
  spender?: string;
381
- /** Which signed-message format to use, and its fields. */
391
+ /**
392
+ * Which signed-message format to use, and its fields. Pass
393
+ * `format: 'loan-agreement'`; `format: 'legacy'` is deprecated and will be
394
+ * removed in the next major version.
395
+ */
382
396
  contractMessage: ContractMessageRequest;
383
397
  /**
384
398
  * The deposit in DISPLAY units, for the generate request only.