@kasufinance/kasu-sdk 2.10.3 → 2.12.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 (41) hide show
  1. package/README.md +28 -1
  2. package/dist/bundle.cjs.js +752 -59
  3. package/dist/bundle.esm.js +750 -60
  4. package/dist/domain/index.d.ts +10 -2
  5. package/dist/domain/index.js +6 -0
  6. package/dist/domain/index.js.map +1 -1
  7. package/dist/domain/strategy-summary.d.ts +47 -0
  8. package/dist/domain/strategy-summary.js +76 -0
  9. package/dist/domain/strategy-summary.js.map +1 -0
  10. package/dist/domain/yield-history.d.ts +122 -0
  11. package/dist/domain/yield-history.js +95 -0
  12. package/dist/domain/yield-history.js.map +1 -0
  13. package/dist/facade/strategies.js +2 -0
  14. package/dist/facade/strategies.js.map +1 -1
  15. package/dist/facade/types.d.ts +13 -0
  16. package/dist/facade/user-portfolio.d.ts +32 -0
  17. package/dist/facade/user-portfolio.js +34 -0
  18. package/dist/facade/user-portfolio.js.map +1 -1
  19. package/dist/index.d.ts +1 -0
  20. package/dist/index.js.map +1 -1
  21. package/dist/services/Portfolio/portfolio.d.ts +37 -0
  22. package/dist/services/Portfolio/portfolio.js +136 -56
  23. package/dist/services/Portfolio/portfolio.js.map +1 -1
  24. package/dist/services/Portfolio/queries.js +15 -3
  25. package/dist/services/Portfolio/queries.js.map +1 -1
  26. package/dist/services/Portfolio/types.d.ts +8 -0
  27. package/dist/services/Portfolio/yield-history.d.ts +177 -0
  28. package/dist/services/Portfolio/yield-history.js +405 -0
  29. package/dist/services/Portfolio/yield-history.js.map +1 -0
  30. package/package.json +2 -1
  31. package/src/domain/index.ts +19 -10
  32. package/src/domain/strategy-summary.ts +88 -0
  33. package/src/domain/yield-history.ts +194 -0
  34. package/src/facade/strategies.ts +2 -0
  35. package/src/facade/types.ts +13 -0
  36. package/src/facade/user-portfolio.ts +40 -3
  37. package/src/index.ts +2 -0
  38. package/src/services/Portfolio/portfolio.ts +263 -153
  39. package/src/services/Portfolio/queries.ts +15 -3
  40. package/src/services/Portfolio/types.ts +10 -0
  41. package/src/services/Portfolio/yield-history.ts +574 -0
@@ -0,0 +1,88 @@
1
+ /**
2
+ * A strategy's SHORT blurb, as plain text: the CMS `liteDescription` first,
3
+ * else the first paragraph of the long `description`, else nothing.
4
+ *
5
+ * Lifted from kasu-ui (`pickShortDescription`, its deposit dialog and landing
6
+ * strategy card) so every app shows the same line, and pinned to its output
7
+ * for every live CMS row. The rules are facts about how the CMS is authored,
8
+ * not formatting preferences:
9
+ *
10
+ * - `liteDescription` IS THE SHORT ONE. `description` is the credit
11
+ * originator's several-paragraph profile; on a card it is a wall.
12
+ * - `"Not Available"` IS A PLACEHOLDER, not editorial. Several rows carry it
13
+ * verbatim, and it must read as missing, not print. Matched on the whole
14
+ * trimmed value, case-insensitively. It applies to `liteDescription` only.
15
+ * - EMPTY OR BLANK IS MISSING. The service maps a pool with no CMS record to
16
+ * `''`.
17
+ * - THE FIELDS HOLD MARKDOWN. `liteDescription` can open with a `**bold**`
18
+ * lead, and the surfaces this feeds render plain text, so the markers are
19
+ * stripped (bold, then italic, then inline code).
20
+ * - AND HARD NEWLINES. Every run of whitespace becomes one space, so a
21
+ * paragraph break inside the value is not a gap in a one-line slot.
22
+ * - NOT SANITISED. "Plain text" means text to render AS text. HTML tags,
23
+ * entities and link syntax (`<script>`, `&lt;`, `[a](javascript:…)`) pass
24
+ * through unchanged, and removing markers can join fragments into a tag
25
+ * (`<scr**i**pt>` becomes `<script>`). Render it as a text node, never as
26
+ * HTML or markdown.
27
+ *
28
+ * Two refinements over the kasu-ui original, neither reachable by any live
29
+ * row: the cleaned text is trimmed (a CRLF value no longer ends in a space),
30
+ * and a `liteDescription` that cleans to nothing (`** **`) counts as missing,
31
+ * so the result is never a blank string.
32
+ *
33
+ * This authors no string of its own. It selects and cleans editorial the CMS
34
+ * already holds, and an absent blurb is `undefined`, never a fallback sentence:
35
+ * what "no blurb" means is the render site's decision.
36
+ *
37
+ * Pure. A value that is not a string reads as missing.
38
+ */
39
+ export function pickShortDescription(
40
+ source: ShortDescriptionSource,
41
+ ): string | undefined {
42
+ const lite = asTrimmed(source.liteDescription);
43
+ if (lite && !NOT_AVAILABLE.test(lite)) {
44
+ const summary = toOneLine(lite);
45
+ if (summary) return summary;
46
+ }
47
+ const long = asTrimmed(source.description);
48
+ if (long) {
49
+ const firstParagraph = long.split(PARAGRAPH_BREAK, 1)[0] ?? long;
50
+ return toOneLine(firstParagraph) || undefined;
51
+ }
52
+ return undefined;
53
+ }
54
+
55
+ /**
56
+ * The two CMS fields `pickShortDescription` reads. A `PoolOverview` and a
57
+ * facade `Strategy` both satisfy it.
58
+ */
59
+ export interface ShortDescriptionSource {
60
+ liteDescription?: string | null;
61
+ description?: string | null;
62
+ }
63
+
64
+ const NOT_AVAILABLE = /^not available$/i;
65
+ const PARAGRAPH_BREAK = /\n\s*\n/;
66
+
67
+ function asTrimmed(value: unknown): string {
68
+ return typeof value === 'string' ? value.trim() : '';
69
+ }
70
+
71
+ /** Markers off, whitespace runs to one space, ends trimmed. `''` when nothing is left. */
72
+ function toOneLine(text: string): string {
73
+ return stripInlineMarkdown(text).replace(/\s+/gu, ' ').trim();
74
+ }
75
+
76
+ /**
77
+ * Inline markdown markers off, in the order bold → italic → code. The bold
78
+ * passes consume every `**` / `__` pair first, so the italic patterns can match
79
+ * a lone `*` / `_` without lookarounds.
80
+ */
81
+ function stripInlineMarkdown(input: string): string {
82
+ return input
83
+ .replace(/\*\*(.+?)\*\*/g, '$1')
84
+ .replace(/__(.+?)__/g, '$1')
85
+ .replace(/\*(.+?)\*/g, '$1')
86
+ .replace(/_(.+?)_/g, '$1')
87
+ .replace(/`([^`]+)`/g, '$1');
88
+ }
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Settled interest per cycle (epoch): what a lender was credited at each close,
3
+ * per pool and tranche, and the one formula that turns a tranche's credited
4
+ * interest into one lender's share of it.
5
+ *
6
+ * **The figures are NET, and only net.** `LendingPool._applyTrancheInterest`
7
+ * takes the performance fee off first and emits `InterestApplied` with the
8
+ * lender side only (`interest × (1 − performanceFee)`); a fixed-term top-up
9
+ * (`_applyFixedRateInterests`) is net of the fee the same way. So the figure
10
+ * this module computes from those records IS what the lender was credited —
11
+ * never take the fee off it again. No gross is offered: the subgraph keeps
12
+ * only TODAY's fee, so a historical gross would be a guess.
13
+ *
14
+ * Units: amounts are display units of the DEPLOYMENT'S OWN stable token (USDC
15
+ * on Base and XDC USDC, AUDD on XDC AUDD) — never converted to USD here;
16
+ * shares are display units of tranche shares, as the subgraph's BigDecimals.
17
+ */
18
+
19
+ /** One fixed-term lock inside a tranche position, at one epoch. */
20
+ export interface EpochLockShares {
21
+ /**
22
+ * The lock earns in this epoch: it started BEFORE the epoch, ends at or
23
+ * after it, and the lender has a share snapshot for the tranche at it.
24
+ */
25
+ active: boolean;
26
+ /** The lock's tranche shares as of the epoch (latest update at or before it). */
27
+ sharesAtEpoch: number;
28
+ /** The lock's tranche shares as of the epoch BEFORE it. */
29
+ sharesBeforeEpoch: number;
30
+ }
31
+
32
+ /** Everything one lender's share of one tranche's credited interest needs. */
33
+ export interface EpochTrancheYieldInput {
34
+ /** The tranche's interest credited at this epoch's close (net of the fee). */
35
+ epochInterest: number;
36
+ /** The lender's tranche shares as of the epoch (fixed-term locks included). */
37
+ userShares: number;
38
+ /** The tranche's total shares as of the epoch. */
39
+ trancheShares: number;
40
+ /**
41
+ * The tranche's CURRENT balance and shares: the price a fixed-term top-up
42
+ * (extra shares) is valued at. The subgraph keeps no per-epoch tranche
43
+ * balance, so an older top-up is valued at today's price — a second-order
44
+ * difference, because the top-up is only the fixed rate's excess over the
45
+ * variable one.
46
+ */
47
+ trancheBalanceNow: number;
48
+ trancheSharesNow: number;
49
+ locks: readonly EpochLockShares[];
50
+ }
51
+
52
+ export interface EpochTrancheYield {
53
+ /** `variable + Σ locks` — the lender's whole yield in the tranche. */
54
+ total: number;
55
+ /** The variable residual: the share-weighted base yield minus the locks' part of it. */
56
+ variable: number;
57
+ /** Each lock's yield, in the order of `input.locks`: its base part plus its top-up. */
58
+ locks: number[];
59
+ }
60
+
61
+ /**
62
+ * The ONE per-epoch formula. `Portfolio.getPortfolioLendingData` runs it for
63
+ * the last settled epoch and `getYieldHistory` for every epoch of a window, so
64
+ * the two figures cannot drift apart.
65
+ *
66
+ * ```
67
+ * base = epochInterest × userShares / trancheShares
68
+ * lockBase = base × lock.sharesBeforeEpoch / userShares (active locks)
69
+ * topUp = (lock.sharesAtEpoch − lock.sharesBeforeEpoch) × trancheBalanceNow / trancheSharesNow
70
+ * variable = base − Σ lockBase
71
+ * total = variable + Σ (lockBase + topUp)
72
+ * ```
73
+ *
74
+ * A zero denominator contributes 0 (a tranche or a lender with no shares
75
+ * earned nothing at that close), never `NaN`.
76
+ */
77
+ export function epochTrancheYield(
78
+ input: EpochTrancheYieldInput,
79
+ ): EpochTrancheYield {
80
+ const base =
81
+ input.trancheShares === 0
82
+ ? 0
83
+ : (input.epochInterest * input.userShares) / input.trancheShares;
84
+
85
+ let variable = base;
86
+ const locks = input.locks.map((lock) => {
87
+ if (!lock.active) return 0;
88
+
89
+ const topUp =
90
+ input.trancheSharesNow === 0
91
+ ? 0
92
+ : ((lock.sharesAtEpoch - lock.sharesBeforeEpoch) *
93
+ input.trancheBalanceNow) /
94
+ input.trancheSharesNow;
95
+
96
+ const lockBase =
97
+ input.userShares === 0
98
+ ? 0
99
+ : (base * lock.sharesBeforeEpoch) / input.userShares;
100
+
101
+ variable -= lockBase;
102
+
103
+ return lockBase + topUp;
104
+ });
105
+
106
+ const total = locks.reduce((sum, lockYield) => sum + lockYield, variable);
107
+
108
+ return { total, variable, locks };
109
+ }
110
+
111
+ /** One lender's settled yield in one tranche at one epoch. */
112
+ export interface YieldHistoryEntry {
113
+ /** The epoch (cycle) number, in the chain's numbering. */
114
+ epochId: number;
115
+ /**
116
+ * When the epoch ended — the next epoch's start — in unix SECONDS (not
117
+ * milliseconds).
118
+ */
119
+ epochEnd: number;
120
+ /** Lowercased lending pool address. */
121
+ poolId: string;
122
+ /** Lowercased tranche address. */
123
+ trancheId: string;
124
+ /**
125
+ * What the close credited the lender in this tranche, AFTER the
126
+ * performance fee, in display units of the deployment's own stable token
127
+ * (AUDD on XDC AUDD — convert before formatting it as USD).
128
+ */
129
+ netYield: number;
130
+ }
131
+
132
+ /** One epoch's yield summed over the entries an aggregation kept. */
133
+ export interface YieldHistoryPoint {
134
+ /** The epoch (cycle) number. */
135
+ epochId: number;
136
+ /** The epoch's end, in unix SECONDS. */
137
+ epochEnd: number;
138
+ /** Net yield summed, in the deployment's own stable token. */
139
+ netYield: number;
140
+ }
141
+
142
+ export interface YieldHistoryFilter {
143
+ /** Keep only this pool (case-insensitive). */
144
+ poolId?: string;
145
+ /** Keep only this tranche (case-insensitive). */
146
+ trancheId?: string;
147
+ }
148
+
149
+ /**
150
+ * Pool/tranche entries → one point per epoch, ascending by `epochId`. With no
151
+ * filter it sums every strategy (the Lend tab's bars); with `poolId` it is one
152
+ * strategy (a strategy page's bars).
153
+ *
154
+ * An epoch appears only if a kept entry has it — there is no zero padding, so
155
+ * an empty input (a new lender) is an empty output.
156
+ *
157
+ * For a fixed-term lock holder the bars can sum to slightly MORE than the
158
+ * lender's lifetime interest (up to about 0.2 %): each top-up is valued at the
159
+ * tranche's CURRENT share price, which has grown since. Show "earned to date"
160
+ * from the lifetime figure, never from this sum.
161
+ */
162
+ export function aggregateYieldHistory(
163
+ entries: readonly YieldHistoryEntry[],
164
+ filter: YieldHistoryFilter = {},
165
+ ): YieldHistoryPoint[] {
166
+ const poolId = filter.poolId?.toLowerCase();
167
+ const trancheId = filter.trancheId?.toLowerCase();
168
+ const byEpoch = new Map<number, YieldHistoryPoint>();
169
+
170
+ for (const entry of entries) {
171
+ if (poolId !== undefined && entry.poolId.toLowerCase() !== poolId) {
172
+ continue;
173
+ }
174
+ if (
175
+ trancheId !== undefined &&
176
+ entry.trancheId.toLowerCase() !== trancheId
177
+ ) {
178
+ continue;
179
+ }
180
+
181
+ const point = byEpoch.get(entry.epochId);
182
+ if (point) {
183
+ point.netYield += entry.netYield;
184
+ continue;
185
+ }
186
+ byEpoch.set(entry.epochId, {
187
+ epochId: entry.epochId,
188
+ epochEnd: entry.epochEnd,
189
+ netYield: entry.netYield,
190
+ });
191
+ }
192
+
193
+ return [...byEpoch.values()].sort((a, b) => a.epochId - b.epochId);
194
+ }
@@ -1,4 +1,5 @@
1
1
  import { selectVisiblePools } from '../domain/pools';
2
+ import { pickShortDescription } from '../domain/strategy-summary';
2
3
  import { DataService } from '../services/DataService/data-service';
3
4
  import {
4
5
  PoolDelegateProfileAndHistory,
@@ -218,6 +219,7 @@ export class StrategiesFacade {
218
219
  subheading: nonEmptyOrUndefined(pool.subheading),
219
220
  description: pool.description,
220
221
  liteDescription: nonEmptyOrUndefined(pool.liteDescription),
222
+ summary: pickShortDescription(pool),
221
223
  isActive: pool.isActive,
222
224
  apy: pool.apy,
223
225
  tvl: pool.totalValueLocked,
@@ -121,6 +121,19 @@ export interface Strategy {
121
121
  * Absent when the record has none, or an empty one.
122
122
  */
123
123
  liteDescription?: string;
124
+ /**
125
+ * The strategy's short blurb as ONE line of plain text, for cards and
126
+ * compact surfaces: `liteDescription` when it is usable, else the first
127
+ * paragraph of `description`, with inline markdown markers stripped and
128
+ * every run of whitespace collapsed to one space. The `"Not Available"`
129
+ * placeholder, an empty or blank value, and a missing one all count as
130
+ * absent. `undefined` when neither field yields a blurb; the SDK never
131
+ * substitutes a sentence of its own. See `pickShortDescription`.
132
+ *
133
+ * Not sanitised: HTML tags and link syntax pass through unchanged. Render
134
+ * it as text, never as HTML or markdown.
135
+ */
136
+ summary?: string;
124
137
  isActive: boolean;
125
138
  /** Weighted-average APY across tranches (decimal, e.g. 0.08 = 8 %). */
126
139
  apy: number;
@@ -1,8 +1,10 @@
1
1
  import { Provider } from '@ethersproject/providers';
2
2
 
3
3
  import { deriveRequestState, RequestState } from '../domain/requests';
4
+ import { YieldHistoryEntry } from '../domain/yield-history';
4
5
  import { DataService } from '../services/DataService/data-service';
5
6
  import { Portfolio } from '../services/Portfolio/portfolio';
7
+ import { YieldHistoryOptions } from '../services/Portfolio/yield-history';
6
8
  import { UserRequest } from '../services/UserLending/types';
7
9
  import { UserLending } from '../services/UserLending/user-lending';
8
10
 
@@ -39,9 +41,8 @@ export class PortfolioFacade {
39
41
  ): Promise<UserPositions> {
40
42
  const currentEpoch = await this._userLending.getCurrentEpoch();
41
43
 
42
- const poolOverviews = await this._dataService.getPoolOverview(
43
- currentEpoch,
44
- );
44
+ const poolOverviews =
45
+ await this._dataService.getPoolOverview(currentEpoch);
45
46
 
46
47
  const pools = await this._portfolio.getPortfolioLendingData(
47
48
  userAddress,
@@ -59,6 +60,42 @@ export class PortfolioFacade {
59
60
  return { pools, summary };
60
61
  }
61
62
 
63
+ /**
64
+ * The lender's settled yield per cycle, per pool and tranche: what each
65
+ * close credited them, NET of the performance fee (the chain credits it
66
+ * net), in display units of the deployment's own stable token (AUDD on
67
+ * XDC AUDD). `epochEnd` is unix seconds. Net only — no gross is offered.
68
+ *
69
+ * Validates `options` before any read (`RangeError` on a malformed one).
70
+ *
71
+ * ```ts
72
+ * const entries = await kasu.portfolio.getYieldHistory('0xUser...', { count: 52 });
73
+ * const allStrategies = aggregateYieldHistory(entries);
74
+ * const oneStrategy = aggregateYieldHistory(entries, { poolId });
75
+ * ```
76
+ *
77
+ * `getPositions`' `totalYieldEarningsLastEpoch` for a pool equals that
78
+ * pool's bar for the epoch of its most recently APPLIED interest — which
79
+ * is older than the last cycle for a pool that has not cleared since (the
80
+ * bar carries that older `epochId`). A lender who held nothing at that
81
+ * epoch has no bar there and a figure of 0; their newest bar is from
82
+ * when they last held shares.
83
+ *
84
+ * For a fixed-term lock holder the bars can sum to slightly more than the
85
+ * lifetime interest (up to about 0.2 %), because each top-up is valued at
86
+ * today's share price — show "earned to date" from the lifetime figure,
87
+ * never from the sum of the bars.
88
+ *
89
+ * `[]` for a lender with no history, and on a LITE deployment whose
90
+ * subgraph lacks the entities; on a full deployment that error throws.
91
+ */
92
+ async getYieldHistory(
93
+ userAddress: string,
94
+ options?: YieldHistoryOptions,
95
+ ): Promise<YieldHistoryEntry[]> {
96
+ return await this._portfolio.getYieldHistory(userAddress, options);
97
+ }
98
+
62
99
  /**
63
100
  * Fetch a user's full transaction history (deposits, withdrawals, cancellations).
64
101
  */
package/src/index.ts CHANGED
@@ -42,6 +42,8 @@ export type {
42
42
  PortfolioTrancheDepositDetails,
43
43
  } from './services/Portfolio/types';
44
44
 
45
+ export type { YieldHistoryOptions } from './services/Portfolio/yield-history';
46
+
45
47
  export type {
46
48
  UserLock,
47
49
  UserBonusData,