@kasufinance/kasu-sdk 2.13.1 → 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.
- package/README.md +68 -5
- package/dist/bundle.cjs.js +1038 -29
- package/dist/bundle.esm.js +1026 -30
- package/dist/domain/index.d.ts +8 -3
- package/dist/domain/index.js +6 -2
- package/dist/domain/index.js.map +1 -1
- package/dist/domain/loan-contract.d.ts +16 -5
- package/dist/domain/loan-contract.js +16 -5
- package/dist/domain/loan-contract.js.map +1 -1
- package/dist/domain/pool-tvl.d.ts +104 -0
- package/dist/domain/pool-tvl.js +179 -0
- package/dist/domain/pool-tvl.js.map +1 -1
- package/dist/domain/requests.js +29 -19
- package/dist/domain/requests.js.map +1 -1
- package/dist/domain/strategy-details.d.ts +262 -0
- package/dist/domain/strategy-details.js +375 -0
- package/dist/domain/strategy-details.js.map +1 -0
- package/dist/facade/index.d.ts +3 -1
- package/dist/facade/index.js +2 -0
- package/dist/facade/index.js.map +1 -1
- package/dist/facade/strategies.d.ts +102 -4
- package/dist/facade/strategies.js +182 -6
- package/dist/facade/strategies.js.map +1 -1
- package/dist/facade/strategy-deployments.d.ts +65 -0
- package/dist/facade/strategy-deployments.js +97 -0
- package/dist/facade/strategy-deployments.js.map +1 -0
- package/dist/facade/types.d.ts +82 -0
- package/dist/flows/deposit-flow.d.ts +19 -5
- package/dist/flows/deposit-flow.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js.map +1 -1
- package/dist/services/DataService/data-service.d.ts +29 -1
- package/dist/services/DataService/data-service.js +80 -9
- package/dist/services/DataService/data-service.js.map +1 -1
- package/dist/services/DataService/tvl-history.d.ts +49 -0
- package/dist/services/DataService/tvl-history.js +89 -0
- package/dist/services/DataService/tvl-history.js.map +1 -1
- package/dist/services/UserLending/queries.js +2 -7
- package/dist/services/UserLending/queries.js.map +1 -1
- package/dist/services/UserLending/subgraph-types.js.map +1 -1
- package/dist/services/UserLending/types.js.map +1 -1
- package/dist/services/UserLending/user-lending.js +72 -16
- package/dist/services/UserLending/user-lending.js.map +1 -1
- package/package.json +1 -1
- package/src/domain/index.ts +34 -3
- package/src/domain/loan-contract.ts +16 -5
- package/src/domain/pool-tvl.ts +281 -0
- package/src/domain/strategy-details.ts +528 -0
- package/src/facade/index.ts +13 -0
- package/src/facade/strategies.ts +231 -11
- package/src/facade/strategy-deployments.ts +161 -0
- package/src/facade/types.ts +85 -0
- package/src/flows/deposit-flow.ts +19 -5
- package/src/index.ts +4 -1
- package/src/services/DataService/data-service.ts +103 -7
- package/src/services/DataService/tvl-history.ts +131 -0
package/src/facade/strategies.ts
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
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
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
PoolTvlHistoryOptions,
|
|
16
|
+
PoolTvlHistoryResult,
|
|
17
|
+
} from '../services/DataService/tvl-history';
|
|
6
18
|
import {
|
|
7
19
|
PoolDelegateProfileAndHistory,
|
|
8
20
|
PoolOverview,
|
|
@@ -12,8 +24,11 @@ import { UserLending } from '../services/UserLending/user-lending';
|
|
|
12
24
|
import {
|
|
13
25
|
DepositLimits,
|
|
14
26
|
FixedTermOption,
|
|
27
|
+
OriginatorHistory,
|
|
15
28
|
PlatformStats,
|
|
16
29
|
Strategy,
|
|
30
|
+
StrategyEndBorrowers,
|
|
31
|
+
StrategyHistory,
|
|
17
32
|
StrategyRiskPerformance,
|
|
18
33
|
StrategyTranche,
|
|
19
34
|
} from './types';
|
|
@@ -32,8 +47,65 @@ export class StrategiesFacade {
|
|
|
32
47
|
* share their Base counterpart's editorial record). Lowercase keys.
|
|
33
48
|
*/
|
|
34
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(),
|
|
35
62
|
) {}
|
|
36
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
|
+
|
|
37
109
|
/**
|
|
38
110
|
* The pool's loss rate and first-loss capital, from its CMS
|
|
39
111
|
* risk-management record. `null` when the pool has no record.
|
|
@@ -45,13 +117,15 @@ export class StrategiesFacade {
|
|
|
45
117
|
* 0..1 fraction.
|
|
46
118
|
*
|
|
47
119
|
* Requires `directusUrl`; without it the call rejects with
|
|
48
|
-
* `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.
|
|
49
123
|
*/
|
|
50
124
|
async getRiskPerformance(
|
|
51
125
|
poolId: string,
|
|
52
126
|
): Promise<StrategyRiskPerformance | null> {
|
|
53
127
|
const metadataId = this._metadataIdOf(poolId);
|
|
54
|
-
const records = await this.
|
|
128
|
+
const records = await this._riskManagementRecords();
|
|
55
129
|
const record = records.find(
|
|
56
130
|
(r) => String(r.poolIdFK).toLowerCase() === metadataId,
|
|
57
131
|
);
|
|
@@ -79,6 +153,10 @@ export class StrategiesFacade {
|
|
|
79
153
|
* deployment every profile lists no pools and this resolves `null` —
|
|
80
154
|
* the same answer kasu-ui's own lookup gives there today. The metadata
|
|
81
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.
|
|
82
160
|
*/
|
|
83
161
|
async getOriginator(
|
|
84
162
|
poolId: string,
|
|
@@ -87,15 +165,102 @@ export class StrategiesFacade {
|
|
|
87
165
|
poolId.toLowerCase(),
|
|
88
166
|
this._metadataIdOf(poolId),
|
|
89
167
|
]);
|
|
90
|
-
const profiles =
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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,
|
|
98
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,
|
|
260
|
+
);
|
|
261
|
+
if (!record || !Array.isArray(record.items)) return null;
|
|
262
|
+
const groups = groupRiskManagementItems(record.items);
|
|
263
|
+
return groups.length > 0 ? groups : null;
|
|
99
264
|
}
|
|
100
265
|
|
|
101
266
|
private _metadataIdOf(poolId: string): string {
|
|
@@ -175,6 +340,28 @@ export class StrategiesFacade {
|
|
|
175
340
|
return await this._dataService.getPoolTvlHistory(poolId, options);
|
|
176
341
|
}
|
|
177
342
|
|
|
343
|
+
/**
|
|
344
|
+
* `getPoolTvlHistory` plus `opening` — the newest clearing before the
|
|
345
|
+
* window, read at its own block like every other point, or `null` when
|
|
346
|
+
* the pool has none — from the same read. Pass the result to
|
|
347
|
+
* `combineTvlHistories` as a leg with its `usdRate`: the opening is what
|
|
348
|
+
* the leg held before its first point, so a window cut at a different
|
|
349
|
+
* time on another chain does not count it 0. Same options and errors as
|
|
350
|
+
* `getPoolTvlHistory`; the extra cost is one clearing row (or, for
|
|
351
|
+
* `fromEpoch`, one small request) and one block.
|
|
352
|
+
*
|
|
353
|
+
* ```ts
|
|
354
|
+
* const leg = await kasu.strategies.getPoolTvlHistoryWindow(poolId, { count: 52 });
|
|
355
|
+
* // { entries: PoolTvlHistoryEntry[], opening: PoolTvlHistoryEntry | null }
|
|
356
|
+
* ```
|
|
357
|
+
*/
|
|
358
|
+
async getPoolTvlHistoryWindow(
|
|
359
|
+
poolId: string,
|
|
360
|
+
options: PoolTvlHistoryOptions = {},
|
|
361
|
+
): Promise<PoolTvlHistoryResult> {
|
|
362
|
+
return await this._dataService.getPoolTvlHistoryWindow(poolId, options);
|
|
363
|
+
}
|
|
364
|
+
|
|
178
365
|
/**
|
|
179
366
|
* Fetch a single strategy by pool ID. Returns `null` if not found.
|
|
180
367
|
*/
|
|
@@ -256,11 +443,44 @@ export class StrategiesFacade {
|
|
|
256
443
|
apyStructure: pool.poolApyStructure,
|
|
257
444
|
thumbnailUrl: pool.thumbnailImageUrl,
|
|
258
445
|
bannerUrl: pool.bannerImageUrl,
|
|
446
|
+
strategyHistory: strategyHistoryOf(pool, this._now()),
|
|
447
|
+
endBorrowers: endBorrowersOf(pool),
|
|
448
|
+
securityStructure: securityStructureOf(pool.security),
|
|
259
449
|
_raw: pool,
|
|
260
450
|
};
|
|
261
451
|
}
|
|
262
452
|
}
|
|
263
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
|
+
|
|
264
484
|
/**
|
|
265
485
|
* A CMS text field as a string, or `undefined` when it is absent, not a
|
|
266
486
|
* string, or empty (blank included) — the CMS stores an emptied field as `''`, and an empty
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { CHAIN_CONFIGS } from './chain-configs';
|
|
2
|
+
import { ChainConfigEntry, SupportedChain } from './types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Strategy ↔ deployment resolution. A lending strategy is not tied to one
|
|
6
|
+
* deployment: Tax Pay runs as one pool on Base (USDC) and another on XDC
|
|
7
|
+
* (AUDD), and the two share one CMS record keyed by the BASE pool id — the
|
|
8
|
+
* canonical id. Each chain config's `poolMetadataMapping` maps its
|
|
9
|
+
* chain-local ids to canonical ones; Base, being canonical, has none.
|
|
10
|
+
*
|
|
11
|
+
* Pure: it reads only the built-in `CHAIN_CONFIGS`. The same rules as
|
|
12
|
+
* kasu-ui's counterpart lookup, so both apps sum a strategy over the same
|
|
13
|
+
* pools.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** The pool that represents a strategy on one deployment. */
|
|
17
|
+
export interface StrategyDeployment {
|
|
18
|
+
chain: SupportedChain;
|
|
19
|
+
/**
|
|
20
|
+
* Chain-local pool id, lowercase — what that chain's subgraph is keyed
|
|
21
|
+
* by.
|
|
22
|
+
*/
|
|
23
|
+
poolId: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* What `strategyDeployments` refuses, as a code:
|
|
28
|
+
*
|
|
29
|
+
* - `pool-id` — not a `0x` address;
|
|
30
|
+
* - `chain` — not a `SupportedChain`;
|
|
31
|
+
* - `wrong-chain` — the mapping places the id on ANOTHER deployment (an XDC
|
|
32
|
+
* chain-local id passed as Base, or a Base id passed as XDC). Querying one
|
|
33
|
+
* chain with another's address reads nothing, silently.
|
|
34
|
+
*/
|
|
35
|
+
export type StrategyDeploymentInputErrorCode =
|
|
36
|
+
| 'pool-id'
|
|
37
|
+
| 'chain'
|
|
38
|
+
| 'wrong-chain';
|
|
39
|
+
|
|
40
|
+
/** What `strategyDeployments` throws for an id it cannot place. */
|
|
41
|
+
export class StrategyDeploymentInputError extends Error {
|
|
42
|
+
readonly code: StrategyDeploymentInputErrorCode;
|
|
43
|
+
|
|
44
|
+
constructor(code: StrategyDeploymentInputErrorCode) {
|
|
45
|
+
super(`strategyDeployments: input refused (${code})`);
|
|
46
|
+
this.name = 'StrategyDeploymentInputError';
|
|
47
|
+
this.code = code;
|
|
48
|
+
Object.setPrototypeOf(this, StrategyDeploymentInputError.prototype);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The canonical deployment: its pool ids ARE the strategies' ids. */
|
|
53
|
+
const CANONICAL_CHAIN: SupportedChain = 'base';
|
|
54
|
+
|
|
55
|
+
const ADDRESS_RE = /^0x[0-9a-f]{40}$/;
|
|
56
|
+
|
|
57
|
+
type MappingConfigs = Readonly<
|
|
58
|
+
Record<SupportedChain, Pick<ChainConfigEntry, 'poolMetadataMapping'>>
|
|
59
|
+
>;
|
|
60
|
+
|
|
61
|
+
/** The deployment's mapping with both sides lowercased; Base never has one. */
|
|
62
|
+
function mappingOf(
|
|
63
|
+
configs: MappingConfigs,
|
|
64
|
+
chain: SupportedChain,
|
|
65
|
+
): [string, string][] {
|
|
66
|
+
if (chain === CANONICAL_CHAIN) return [];
|
|
67
|
+
return Object.entries(configs[chain].poolMetadataMapping ?? {}).map(
|
|
68
|
+
([local, canonical]) => [local.toLowerCase(), canonical.toLowerCase()],
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* `strategyDeployments` over any configs — the export reads `CHAIN_CONFIGS`;
|
|
74
|
+
* the spec passes its own to cover shapes the built-in mapping lacks.
|
|
75
|
+
*/
|
|
76
|
+
export function resolveStrategyDeployments(
|
|
77
|
+
configs: MappingConfigs,
|
|
78
|
+
poolId: string,
|
|
79
|
+
chain: SupportedChain,
|
|
80
|
+
): StrategyDeployment[] {
|
|
81
|
+
const id = typeof poolId === 'string' ? poolId.trim().toLowerCase() : '';
|
|
82
|
+
if (!ADDRESS_RE.test(id)) {
|
|
83
|
+
throw new StrategyDeploymentInputError('pool-id');
|
|
84
|
+
}
|
|
85
|
+
if (
|
|
86
|
+
typeof chain !== 'string' ||
|
|
87
|
+
!Object.prototype.hasOwnProperty.call(configs, chain)
|
|
88
|
+
) {
|
|
89
|
+
throw new StrategyDeploymentInputError('chain');
|
|
90
|
+
}
|
|
91
|
+
const chains = Object.keys(configs) as SupportedChain[];
|
|
92
|
+
const mappings = chains.map((c) => [c, mappingOf(configs, c)] as const);
|
|
93
|
+
const isLocalElsewhere = mappings.some(
|
|
94
|
+
([c, mapping]) =>
|
|
95
|
+
c !== chain && mapping.some(([local]) => local === id),
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
let canonical: string;
|
|
99
|
+
if (chain === CANONICAL_CHAIN) {
|
|
100
|
+
// A Base id is its own Base pool — unless the mapping says it is
|
|
101
|
+
// another deployment's chain-local id, which names no Base pool.
|
|
102
|
+
if (isLocalElsewhere) {
|
|
103
|
+
throw new StrategyDeploymentInputError('wrong-chain');
|
|
104
|
+
}
|
|
105
|
+
canonical = id;
|
|
106
|
+
} else {
|
|
107
|
+
const own = mappingOf(configs, chain).find(([local]) => local === id);
|
|
108
|
+
if (own) {
|
|
109
|
+
canonical = own[1];
|
|
110
|
+
} else {
|
|
111
|
+
const isCanonical = mappings.some(([, mapping]) =>
|
|
112
|
+
mapping.some(([, target]) => target === id),
|
|
113
|
+
);
|
|
114
|
+
if (isLocalElsewhere || isCanonical) {
|
|
115
|
+
throw new StrategyDeploymentInputError('wrong-chain');
|
|
116
|
+
}
|
|
117
|
+
// Unmapped off Base: a strategy of one on its own chain (every
|
|
118
|
+
// XDC USDC pool today). Claiming a Base counterpart would read
|
|
119
|
+
// Base with an XDC address.
|
|
120
|
+
return [{ chain, poolId: id }];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const found: StrategyDeployment[] = [
|
|
125
|
+
{ chain: CANONICAL_CHAIN, poolId: canonical },
|
|
126
|
+
];
|
|
127
|
+
for (const [c, mapping] of mappings) {
|
|
128
|
+
for (const [local, target] of mapping) {
|
|
129
|
+
if (target === canonical) found.push({ chain: c, poolId: local });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return found;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Every deployment a strategy has a pool on, canonical (Base) first, then the
|
|
137
|
+
* others in `CHAIN_CONFIGS` order — the legs to fetch and pass to
|
|
138
|
+
* `combineTvlHistories`. One entry for a strategy that lives on one chain
|
|
139
|
+
* only (PayFi, on Base).
|
|
140
|
+
*
|
|
141
|
+
* `chain` is the deployment `poolId` was read on — required, because an
|
|
142
|
+
* address alone does not say which chain it belongs to. From either side of
|
|
143
|
+
* a mapping the answer is the same set: Tax Pay's Base id on `'base'` and
|
|
144
|
+
* its XDC id on `'xdc'` both return both pools. An id the mapping does not
|
|
145
|
+
* know is a strategy of one on its own chain. Ids are compared
|
|
146
|
+
* case-insensitively and returned lowercase.
|
|
147
|
+
*
|
|
148
|
+
* Throws `StrategyDeploymentInputError` for a malformed id, an unknown chain,
|
|
149
|
+
* or an id the mapping places on another deployment (`'wrong-chain'`).
|
|
150
|
+
*
|
|
151
|
+
* ```ts
|
|
152
|
+
* strategyDeployments(strategy.id, 'base');
|
|
153
|
+
* // [{ chain: 'base', poolId: '0x…' }, { chain: 'xdc', poolId: '0x…' }]
|
|
154
|
+
* ```
|
|
155
|
+
*/
|
|
156
|
+
export function strategyDeployments(
|
|
157
|
+
poolId: string,
|
|
158
|
+
chain: SupportedChain,
|
|
159
|
+
): StrategyDeployment[] {
|
|
160
|
+
return resolveStrategyDeployments(CHAIN_CONFIGS, poolId, chain);
|
|
161
|
+
}
|
package/src/facade/types.ts
CHANGED
|
@@ -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).
|
|
49
|
-
* reconstructs that string byte-for-byte to verify the
|
|
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
|
|
270
|
-
*
|
|
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
|
-
/**
|
|
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.
|
package/src/index.ts
CHANGED
|
@@ -45,7 +45,10 @@ export type {
|
|
|
45
45
|
|
|
46
46
|
export type { YieldHistoryOptions } from './services/Portfolio/yield-history';
|
|
47
47
|
|
|
48
|
-
export type {
|
|
48
|
+
export type {
|
|
49
|
+
PoolTvlHistoryOptions,
|
|
50
|
+
PoolTvlHistoryResult,
|
|
51
|
+
} from './services/DataService/tvl-history';
|
|
49
52
|
export {
|
|
50
53
|
PastStateUnavailableError,
|
|
51
54
|
PoolTvlHistoryAbortedError,
|