@kasufinance/kasu-sdk 2.15.0 → 2.17.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 (42) hide show
  1. package/README.md +17 -1
  2. package/dist/bundle.cjs.js +1144 -221
  3. package/dist/bundle.esm.js +1141 -222
  4. package/dist/domain/index.d.ts +2 -2
  5. package/dist/domain/index.js +1 -1
  6. package/dist/domain/index.js.map +1 -1
  7. package/dist/domain/yield-history.d.ts +25 -1
  8. package/dist/domain/yield-history.js +41 -11
  9. package/dist/domain/yield-history.js.map +1 -1
  10. package/dist/domain/yields.d.ts +200 -15
  11. package/dist/domain/yields.js +211 -14
  12. package/dist/domain/yields.js.map +1 -1
  13. package/dist/services/Locking/locking.d.ts +12 -0
  14. package/dist/services/Locking/locking.js +16 -0
  15. package/dist/services/Locking/locking.js.map +1 -1
  16. package/dist/services/Portfolio/portfolio.d.ts +54 -1
  17. package/dist/services/Portfolio/portfolio.js +357 -80
  18. package/dist/services/Portfolio/portfolio.js.map +1 -1
  19. package/dist/services/Portfolio/queries.d.ts +16 -0
  20. package/dist/services/Portfolio/queries.js +119 -2
  21. package/dist/services/Portfolio/queries.js.map +1 -1
  22. package/dist/services/Portfolio/types.d.ts +94 -6
  23. package/dist/services/Portfolio/yield-history.d.ts +148 -10
  24. package/dist/services/Portfolio/yield-history.js +241 -31
  25. package/dist/services/Portfolio/yield-history.js.map +1 -1
  26. package/dist/services/UserLending/user-lending.d.ts +16 -0
  27. package/dist/services/UserLending/user-lending.js +24 -0
  28. package/dist/services/UserLending/user-lending.js.map +1 -1
  29. package/dist/services/shared.d.ts +7 -0
  30. package/dist/services/shared.js +48 -0
  31. package/dist/services/shared.js.map +1 -1
  32. package/package.json +1 -1
  33. package/src/domain/index.ts +5 -1
  34. package/src/domain/yield-history.ts +47 -11
  35. package/src/domain/yields.ts +257 -15
  36. package/src/services/Locking/locking.ts +17 -0
  37. package/src/services/Portfolio/portfolio.ts +559 -126
  38. package/src/services/Portfolio/queries.ts +119 -2
  39. package/src/services/Portfolio/types.ts +91 -6
  40. package/src/services/Portfolio/yield-history.ts +405 -56
  41. package/src/services/UserLending/user-lending.ts +46 -0
  42. package/src/services/shared.ts +48 -0
@@ -1,4 +1,4 @@
1
- import { YieldHistoryEntry } from '../../domain/yield-history';
1
+ import { EpochTrancheYield, YieldHistoryEntry } from '../../domain/yield-history';
2
2
  /**
3
3
  * Which settled epochs a history covers. Epochs are counted in the chain's
4
4
  * numbering (`SystemVariables.currentEpochNumber`). Only SETTLED epochs exist
@@ -40,13 +40,20 @@ export declare function resolveYieldHistoryWindow(options: YieldHistoryOptions,
40
40
  */
41
41
  export declare const yieldHistoryPositionsQuery: string;
42
42
  /**
43
- * Round trip 2 onwards: the four per-epoch series, one page of each per
43
+ * Round trip 2 onwards: the six per-epoch series, one page of each per
44
44
  * request, cursor-paginated by `id`. A series that is exhausted is skipped with
45
45
  * `@include`, so a history costs as many requests as its LONGEST series has
46
46
  * pages — normally one — never one per epoch.
47
47
  *
48
48
  * Share snapshots are read up to the window's end with no lower bound: the
49
- * value AS OF the first epoch may come from an update long before it.
49
+ * value AS OF the first epoch may come from an update long before it. Each
50
+ * carries its `timestamp`, and the window's clearings their start, because a
51
+ * cycle's interest is paired with the shares in force when the clearing
52
+ * APPLIED it (`sharesAtInterest`), which the epoch tag alone gets wrong for a
53
+ * withdrawal accepted between clearings — so the lender's updates are read one
54
+ * tag past the window (`$userSharesToEpoch`), where the subgraph puts it.
55
+ * Every share exit between clearings is read too (`trancheSharesAtInterest`
56
+ * rebuilds the tranche total from the previous clearing's snapshot).
50
57
  */
51
58
  export declare const yieldHistoryPageQuery: string;
52
59
  export interface YieldHistoryPositionsResult {
@@ -57,6 +64,8 @@ export interface YieldHistoryPositionsResult {
57
64
  };
58
65
  lendingPoolTrancheUserDetails: {
59
66
  id: string;
67
+ /** The ledger's share count (accepted deposits − withdrawals ± top-ups). */
68
+ shares?: string;
60
69
  tranche: {
61
70
  id: string;
62
71
  balance: string;
@@ -71,6 +80,12 @@ export interface YieldHistoryPositionsResult {
71
80
  }[];
72
81
  }[];
73
82
  } | null;
83
+ /** The block the subgraph answered at. */
84
+ _meta?: {
85
+ block: {
86
+ number: number;
87
+ };
88
+ };
74
89
  }
75
90
  /** One share snapshot: the value from `epochId` on, until the next one. */
76
91
  export interface ShareUpdateRow {
@@ -79,6 +94,13 @@ export interface ShareUpdateRow {
79
94
  shareUpdatesIndex: number;
80
95
  shares: string;
81
96
  }
97
+ /** A tranche or lender share snapshot, with the block time it was written at. */
98
+ export interface TimedShareUpdateRow extends ShareUpdateRow {
99
+ /** Unix seconds. */
100
+ timestamp: string;
101
+ }
102
+ /** What the share selection reads from a snapshot — no entity id needed. */
103
+ export type TimedShareSnapshot = Pick<TimedShareUpdateRow, 'epochId' | 'shareUpdatesIndex' | 'shares' | 'timestamp'>;
82
104
  export interface InterestRow {
83
105
  id: string;
84
106
  epochId: string;
@@ -87,14 +109,40 @@ export interface InterestRow {
87
109
  id: string;
88
110
  };
89
111
  }
112
+ /** A pool's clearing for one epoch: its first transaction applied the interest. */
113
+ export interface ClearingRow {
114
+ id: string;
115
+ epochId: string;
116
+ /** Unix seconds of the clearing's first transaction. */
117
+ startTimestamp: string;
118
+ lendingPool: {
119
+ id: string;
120
+ };
121
+ }
122
+ /**
123
+ * Shares leaving a tranche (any lender): an accepted withdrawal or an
124
+ * immediate one. Each carries the pool's clearing epoch at the time
125
+ * (`epochClearingId`) — so one between clearings E − 1 and E is tagged E.
126
+ */
127
+ export interface ShareExitRow {
128
+ id: string;
129
+ type: 'WithdrawalAccepted' | 'ImmediateWithdrawalExecuted';
130
+ epochId: string;
131
+ /** Unix seconds. */
132
+ createdOn: string;
133
+ sharesAmount: string | null;
134
+ tranche: {
135
+ id: string;
136
+ };
137
+ }
90
138
  export interface YieldHistoryPageResult {
91
139
  interest?: InterestRow[];
92
- trancheShares?: (ShareUpdateRow & {
140
+ trancheShares?: (TimedShareUpdateRow & {
93
141
  lendingPoolTranche: {
94
142
  id: string;
95
143
  };
96
144
  })[];
97
- userShares?: (ShareUpdateRow & {
145
+ userShares?: (TimedShareUpdateRow & {
98
146
  lendingPoolTrancheUserDetails: {
99
147
  id: string;
100
148
  };
@@ -104,17 +152,19 @@ export interface YieldHistoryPageResult {
104
152
  id: string;
105
153
  };
106
154
  })[];
155
+ clearings?: ClearingRow[];
156
+ withdrawals?: ShareExitRow[];
107
157
  }
108
158
  /** Everything `buildYieldHistory` reads, keyed by the subgraph's entity ids. */
109
159
  export interface YieldHistoryRead {
110
160
  positions: YieldHistoryPositionsResult;
111
161
  interest: InterestRow[];
112
- trancheShares: (ShareUpdateRow & {
162
+ trancheShares: (TimedShareUpdateRow & {
113
163
  lendingPoolTranche: {
114
164
  id: string;
115
165
  };
116
166
  })[];
117
- userShares: (ShareUpdateRow & {
167
+ userShares: (TimedShareUpdateRow & {
118
168
  lendingPoolTrancheUserDetails: {
119
169
  id: string;
120
170
  };
@@ -124,6 +174,8 @@ export interface YieldHistoryRead {
124
174
  id: string;
125
175
  };
126
176
  })[];
177
+ clearings: ClearingRow[];
178
+ withdrawals: ShareExitRow[];
127
179
  }
128
180
  /** The chain's epoch clock: epoch E ends at `epochZeroStart + (E + 1) × duration`. */
129
181
  export interface EpochClock {
@@ -132,10 +184,11 @@ export interface EpochClock {
132
184
  }
133
185
  type PageRequest = (variables: Record<string, unknown>) => Promise<YieldHistoryPageResult>;
134
186
  /**
135
- * Fetch the four series for the window, paginating each by `id` until a short
187
+ * Fetch the six series for the window, paginating each by `id` until a short
136
188
  * page. Returns the rows and how many requests it took.
137
189
  */
138
190
  export declare function fetchYieldHistorySeries(request: PageRequest, ids: {
191
+ pools: string[];
139
192
  tranches: string[];
140
193
  userTranches: string[];
141
194
  locks: string[];
@@ -149,12 +202,97 @@ export declare function fetchYieldHistorySeries(request: PageRequest, ids: {
149
202
  * `lendingPortfolioQuery` selects with `orderBy: shareUpdatesIndex, desc,
150
203
  * first: 1, where: { epochId_lt: epoch + 1 }`. `undefined` when none.
151
204
  */
152
- export declare function sharesAsOf(updates: readonly ShareUpdateRow[], epoch: number): ShareUpdateRow | undefined;
205
+ export declare function sharesAsOf<T extends Pick<ShareUpdateRow, 'epochId' | 'shareUpdatesIndex' | 'shares'>>(updates: readonly T[], epoch: number): T | undefined;
206
+ /**
207
+ * The snapshot in force when the clearing of `epoch` APPLIED that epoch's
208
+ * interest — the shares the interest was actually spread over.
209
+ *
210
+ * A clearing applies interest FIRST (`ClearingCoordinator.doClearing`: the
211
+ * `applyInterests` call before steps 1–5), then accepts deposits and
212
+ * withdrawals. So an update is in force for epoch E's interest when it is
213
+ * tagged `≤ E` (the subgraph tags every update written during clearing E, or
214
+ * after it, `E + 1` or later), OR when it was written before clearing E began
215
+ * (`timestamp < clearingStart`). The second test is the one that matters: a
216
+ * withdrawal the pool manager accepts BETWEEN clearings
217
+ * (`forceAcceptWithdrawalRequest`) is tagged `E + 1` by the subgraph, as if a
218
+ * clearing had accepted it, yet the shares were gone before clearing E.
219
+ *
220
+ * Without a clearing start (none recorded) it is `sharesAsOf` — the tag alone.
221
+ */
222
+ export declare function sharesAtInterest<T extends TimedShareSnapshot>(updates: readonly T[], epoch: number, clearingStart: number | undefined): T | undefined;
223
+ /**
224
+ * The tranche's total shares when the clearing of `epoch` applied its
225
+ * interest.
226
+ *
227
+ * The subgraph writes a tranche snapshot when a clearing ENDS (tagged with
228
+ * the next epoch) and at each immediate withdrawal, but none when the manager
229
+ * accepts a withdrawal between clearings — so the newest snapshot can predate
230
+ * shares that had already left. Instead of guessing the order of events in a
231
+ * block, the total is rebuilt from the one snapshot whose place is certain:
232
+ * the FIRST one tagged `epoch`, written when clearing `epoch − 1` ended (the
233
+ * tag itself starts there). Every share exit between that and clearing
234
+ * `epoch` is tagged `epoch` (the pool's clearing epoch at the time), and none
235
+ * can happen during a clearing (`notPendingClearing`,
236
+ * `verifyClearingNotPending`), while deposits and top-ups only happen inside
237
+ * one. So: that snapshot less every exit tagged `epoch` before the clearing's
238
+ * start (the clearing's own acceptances come at or after it).
239
+ *
240
+ * Without a clearing start, or without a snapshot tagged `epoch` (a tranche's
241
+ * first clearing), it is the snapshot in force by tag alone.
242
+ */
243
+ export declare function trancheSharesAtInterest(snapshots: readonly TimedShareSnapshot[], exits: readonly Pick<ShareExitRow, 'epochId' | 'createdOn' | 'sharesAmount'>[], epoch: number, clearingStart: number | undefined): number;
244
+ /** A fixed-term lock inside one lender's tranche position. */
245
+ export interface PositionLock {
246
+ epochLockStart: string;
247
+ epochLockEnd: string;
248
+ initialTrancheShares: string;
249
+ /** The lock's share snapshots (paired by tag: its tags are right). */
250
+ updates: readonly Pick<ShareUpdateRow, 'epochId' | 'shareUpdatesIndex' | 'shares'>[];
251
+ }
252
+ /** Everything one lender's tranche position needs to price one epoch. */
253
+ export interface PositionSeries {
254
+ trancheBalanceNow: number;
255
+ trancheSharesNow: number;
256
+ userUpdates: readonly TimedShareSnapshot[];
257
+ trancheUpdates: readonly TimedShareSnapshot[];
258
+ exits: readonly Pick<ShareExitRow, 'epochId' | 'createdOn' | 'sharesAmount'>[];
259
+ locks: readonly PositionLock[];
260
+ }
261
+ /**
262
+ * THE per-position, per-epoch selection both `getYieldHistory` and
263
+ * `getPortfolioLendingData` run: the shares in force when clearing `epoch`
264
+ * applied its interest (`sharesAtInterest`, `trancheSharesAtInterest`), each
265
+ * lock's snapshots as of `epoch` and `epoch − 1`, then `epochTrancheYield`.
266
+ */
267
+ export declare function positionEpochYield(series: PositionSeries, epoch: number, epochInterest: number, clearingStart: number | undefined): {
268
+ yield: EpochTrancheYield;
269
+ userShares: number;
270
+ };
271
+ /** One lender's tranche position, as the positions and portfolio queries read it. */
272
+ export interface PositionRef {
273
+ id: string;
274
+ tranche: {
275
+ id: string;
276
+ balance: string;
277
+ shares: string;
278
+ };
279
+ userLendingPoolTrancheFixedTermDepositLocks: {
280
+ id?: string;
281
+ epochLockStart: string;
282
+ epochLockEnd: string;
283
+ initialTrancheShares: string;
284
+ }[];
285
+ }
286
+ /** A position's `PositionSeries` out of the fetched history series. */
287
+ export declare function positionSeriesFrom(rows: Omit<YieldHistoryRead, 'positions'>, position: PositionRef): PositionSeries;
288
+ /** `pool → epoch → clearing start`, from the clearing rows. */
289
+ export declare function clearingStarts(clearings: readonly ClearingRow[]): Map<string, Map<number, number>>;
153
290
  /**
154
291
  * Pure: the fetched series → one entry per pool, tranche and settled epoch in
155
292
  * which the lender held shares (or a lock earned a top-up). Each entry runs the
156
293
  * same `epochTrancheYield` as the portfolio's last-epoch figure, with the same
157
- * share selection, so the newest entry equals that figure.
294
+ * share selection (`sharesAtInterest`, `trancheSharesAtInterest`), so the
295
+ * newest entry equals that figure.
158
296
  *
159
297
  * Sorted by `epochId`, then `poolId`, then `trancheId`.
160
298
  */
@@ -72,6 +72,7 @@ export const yieldHistoryPositionsQuery = gql `
72
72
  }
73
73
  lendingPoolTrancheUserDetails(first: $first) {
74
74
  id
75
+ shares
75
76
  tranche {
76
77
  id
77
78
  balance
@@ -86,32 +87,50 @@ export const yieldHistoryPositionsQuery = gql `
86
87
  }
87
88
  }
88
89
  }
90
+ _meta {
91
+ block {
92
+ number
93
+ }
94
+ }
89
95
  }
90
96
  `;
91
97
  /**
92
- * Round trip 2 onwards: the four per-epoch series, one page of each per
98
+ * Round trip 2 onwards: the six per-epoch series, one page of each per
93
99
  * request, cursor-paginated by `id`. A series that is exhausted is skipped with
94
100
  * `@include`, so a history costs as many requests as its LONGEST series has
95
101
  * pages — normally one — never one per epoch.
96
102
  *
97
103
  * Share snapshots are read up to the window's end with no lower bound: the
98
- * value AS OF the first epoch may come from an update long before it.
104
+ * value AS OF the first epoch may come from an update long before it. Each
105
+ * carries its `timestamp`, and the window's clearings their start, because a
106
+ * cycle's interest is paired with the shares in force when the clearing
107
+ * APPLIED it (`sharesAtInterest`), which the epoch tag alone gets wrong for a
108
+ * withdrawal accepted between clearings — so the lender's updates are read one
109
+ * tag past the window (`$userSharesToEpoch`), where the subgraph puts it.
110
+ * Every share exit between clearings is read too (`trancheSharesAtInterest`
111
+ * rebuilds the tranche total from the previous clearing's snapshot).
99
112
  */
100
113
  export const yieldHistoryPageQuery = gql `
101
114
  query YieldHistoryPage(
115
+ $pools: [String!]!
102
116
  $tranches: [String!]!
103
117
  $userTranches: [String!]!
104
118
  $locks: [String!]!
105
119
  $fromEpoch: BigInt!
106
120
  $toEpoch: BigInt!
121
+ $userSharesToEpoch: BigInt!
107
122
  $interestCursor: ID!
108
123
  $trancheSharesCursor: ID!
109
124
  $userSharesCursor: ID!
110
125
  $lockSharesCursor: ID!
126
+ $clearingsCursor: ID!
127
+ $withdrawalsCursor: ID!
111
128
  $withInterest: Boolean!
112
129
  $withTrancheShares: Boolean!
113
130
  $withUserShares: Boolean!
114
131
  $withLockShares: Boolean!
132
+ $withClearings: Boolean!
133
+ $withWithdrawals: Boolean!
115
134
  $first: Int!
116
135
  ) {
117
136
  interest: lendingPoolTrancheEpochInterests(
@@ -146,6 +165,7 @@ export const yieldHistoryPageQuery = gql `
146
165
  epochId
147
166
  shareUpdatesIndex
148
167
  shares
168
+ timestamp
149
169
  lendingPoolTranche {
150
170
  id
151
171
  }
@@ -157,13 +177,14 @@ export const yieldHistoryPageQuery = gql `
157
177
  where: {
158
178
  id_gt: $userSharesCursor
159
179
  lendingPoolTrancheUserDetails_in: $userTranches
160
- epochId_lte: $toEpoch
180
+ epochId_lte: $userSharesToEpoch
161
181
  }
162
182
  ) @include(if: $withUserShares) {
163
183
  id
164
184
  epochId
165
185
  shareUpdatesIndex
166
186
  shares
187
+ timestamp
167
188
  lendingPoolTrancheUserDetails {
168
189
  id
169
190
  }
@@ -186,10 +207,49 @@ export const yieldHistoryPageQuery = gql `
186
207
  id
187
208
  }
188
209
  }
210
+ clearings: lendingPoolClearings(
211
+ first: $first
212
+ orderBy: id
213
+ orderDirection: asc
214
+ where: {
215
+ id_gt: $clearingsCursor
216
+ lendingPool_in: $pools
217
+ epochId_gte: $fromEpoch
218
+ epochId_lte: $toEpoch
219
+ }
220
+ ) @include(if: $withClearings) {
221
+ id
222
+ epochId
223
+ startTimestamp
224
+ lendingPool {
225
+ id
226
+ }
227
+ }
228
+ withdrawals: userRequestEvents(
229
+ first: $first
230
+ orderBy: id
231
+ orderDirection: asc
232
+ where: {
233
+ id_gt: $withdrawalsCursor
234
+ type_in: [WithdrawalAccepted, ImmediateWithdrawalExecuted]
235
+ tranche_in: $tranches
236
+ epochId_gte: $fromEpoch
237
+ epochId_lte: $toEpoch
238
+ }
239
+ ) @include(if: $withWithdrawals) {
240
+ id
241
+ type
242
+ epochId
243
+ createdOn
244
+ sharesAmount
245
+ tranche {
246
+ id
247
+ }
248
+ }
189
249
  }
190
250
  `;
191
251
  /**
192
- * Fetch the four series for the window, paginating each by `id` until a short
252
+ * Fetch the six series for the window, paginating each by `id` until a short
193
253
  * page. Returns the rows and how many requests it took.
194
254
  */
195
255
  export function fetchYieldHistorySeries(request, ids, window) {
@@ -200,6 +260,8 @@ export function fetchYieldHistorySeries(request, ids, window) {
200
260
  trancheShares: [],
201
261
  userShares: [],
202
262
  lockShares: [],
263
+ clearings: [],
264
+ withdrawals: [],
203
265
  };
204
266
  const series = [
205
267
  { key: 'interest', flag: 'withInterest', cursor: 'interestCursor' },
@@ -218,18 +280,32 @@ export function fetchYieldHistorySeries(request, ids, window) {
218
280
  flag: 'withLockShares',
219
281
  cursor: 'lockSharesCursor',
220
282
  },
283
+ {
284
+ key: 'clearings',
285
+ flag: 'withClearings',
286
+ cursor: 'clearingsCursor',
287
+ },
288
+ {
289
+ key: 'withdrawals',
290
+ flag: 'withWithdrawals',
291
+ cursor: 'withdrawalsCursor',
292
+ },
221
293
  ];
222
294
  const cursors = {
223
295
  interest: '',
224
296
  trancheShares: '',
225
297
  userShares: '',
226
298
  lockShares: '',
299
+ clearings: '',
300
+ withdrawals: '',
227
301
  };
228
302
  const open = {
229
303
  interest: ids.tranches.length > 0,
230
304
  trancheShares: ids.tranches.length > 0,
231
305
  userShares: ids.userTranches.length > 0,
232
306
  lockShares: ids.locks.length > 0,
307
+ clearings: ids.pools.length > 0,
308
+ withdrawals: ids.tranches.length > 0,
233
309
  };
234
310
  let requests = 0;
235
311
  while (series.some(({ key }) => open[key])) {
@@ -237,11 +313,15 @@ export function fetchYieldHistorySeries(request, ids, window) {
237
313
  throw new Error(`getYieldHistory: more than ${String(YIELD_HISTORY_MAX_PAGES)} pages for one lender`);
238
314
  }
239
315
  const variables = {
316
+ pools: ids.pools,
240
317
  tranches: ids.tranches,
241
318
  userTranches: ids.userTranches,
242
319
  locks: ids.locks,
243
320
  fromEpoch: String(window.fromEpoch),
244
321
  toEpoch: String(window.toEpoch),
322
+ // One tag further for the lender: a withdrawal accepted between
323
+ // clearings is tagged with the epoch AFTER the one it counts in.
324
+ userSharesToEpoch: String(window.toEpoch + 1),
245
325
  first: YIELD_HISTORY_PAGE_SIZE,
246
326
  };
247
327
  for (const { key, flag, cursor } of series) {
@@ -286,6 +366,144 @@ export function sharesAsOf(updates, epoch) {
286
366
  }
287
367
  return found;
288
368
  }
369
+ /**
370
+ * The snapshot in force when the clearing of `epoch` APPLIED that epoch's
371
+ * interest — the shares the interest was actually spread over.
372
+ *
373
+ * A clearing applies interest FIRST (`ClearingCoordinator.doClearing`: the
374
+ * `applyInterests` call before steps 1–5), then accepts deposits and
375
+ * withdrawals. So an update is in force for epoch E's interest when it is
376
+ * tagged `≤ E` (the subgraph tags every update written during clearing E, or
377
+ * after it, `E + 1` or later), OR when it was written before clearing E began
378
+ * (`timestamp < clearingStart`). The second test is the one that matters: a
379
+ * withdrawal the pool manager accepts BETWEEN clearings
380
+ * (`forceAcceptWithdrawalRequest`) is tagged `E + 1` by the subgraph, as if a
381
+ * clearing had accepted it, yet the shares were gone before clearing E.
382
+ *
383
+ * Without a clearing start (none recorded) it is `sharesAsOf` — the tag alone.
384
+ */
385
+ export function sharesAtInterest(updates, epoch, clearingStart) {
386
+ let found;
387
+ for (const update of updates) {
388
+ const inForce = Number(update.epochId) <= epoch ||
389
+ (clearingStart !== undefined &&
390
+ Number(update.timestamp) < clearingStart);
391
+ if (!inForce)
392
+ continue;
393
+ if (!found || update.shareUpdatesIndex > found.shareUpdatesIndex) {
394
+ found = update;
395
+ }
396
+ }
397
+ return found;
398
+ }
399
+ /**
400
+ * The tranche's total shares when the clearing of `epoch` applied its
401
+ * interest.
402
+ *
403
+ * The subgraph writes a tranche snapshot when a clearing ENDS (tagged with
404
+ * the next epoch) and at each immediate withdrawal, but none when the manager
405
+ * accepts a withdrawal between clearings — so the newest snapshot can predate
406
+ * shares that had already left. Instead of guessing the order of events in a
407
+ * block, the total is rebuilt from the one snapshot whose place is certain:
408
+ * the FIRST one tagged `epoch`, written when clearing `epoch − 1` ended (the
409
+ * tag itself starts there). Every share exit between that and clearing
410
+ * `epoch` is tagged `epoch` (the pool's clearing epoch at the time), and none
411
+ * can happen during a clearing (`notPendingClearing`,
412
+ * `verifyClearingNotPending`), while deposits and top-ups only happen inside
413
+ * one. So: that snapshot less every exit tagged `epoch` before the clearing's
414
+ * start (the clearing's own acceptances come at or after it).
415
+ *
416
+ * Without a clearing start, or without a snapshot tagged `epoch` (a tranche's
417
+ * first clearing), it is the snapshot in force by tag alone.
418
+ */
419
+ export function trancheSharesAtInterest(snapshots, exits, epoch, clearingStart) {
420
+ var _a, _b, _c;
421
+ let base;
422
+ if (clearingStart !== undefined) {
423
+ for (const snapshot of snapshots) {
424
+ if (Number(snapshot.epochId) !== epoch)
425
+ continue;
426
+ if (!base || snapshot.shareUpdatesIndex < base.shareUpdatesIndex) {
427
+ base = snapshot;
428
+ }
429
+ }
430
+ }
431
+ if (clearingStart === undefined || !base) {
432
+ return parseFloat((_b = (_a = sharesAsOf(snapshots, epoch)) === null || _a === void 0 ? void 0 : _a.shares) !== null && _b !== void 0 ? _b : '0');
433
+ }
434
+ let shares = parseFloat(base.shares);
435
+ for (const exit of exits) {
436
+ if (Number(exit.epochId) === epoch &&
437
+ Number(exit.createdOn) < clearingStart) {
438
+ shares -= parseFloat((_c = exit.sharesAmount) !== null && _c !== void 0 ? _c : '0');
439
+ }
440
+ }
441
+ return shares;
442
+ }
443
+ /**
444
+ * THE per-position, per-epoch selection both `getYieldHistory` and
445
+ * `getPortfolioLendingData` run: the shares in force when clearing `epoch`
446
+ * applied its interest (`sharesAtInterest`, `trancheSharesAtInterest`), each
447
+ * lock's snapshots as of `epoch` and `epoch − 1`, then `epochTrancheYield`.
448
+ */
449
+ export function positionEpochYield(series, epoch, epochInterest, clearingStart) {
450
+ var _a;
451
+ const userSnapshot = sharesAtInterest(series.userUpdates, epoch, clearingStart);
452
+ const userShares = parseFloat((_a = userSnapshot === null || userSnapshot === void 0 ? void 0 : userSnapshot.shares) !== null && _a !== void 0 ? _a : '0');
453
+ const locks = series.locks.map((lock) => {
454
+ var _a, _b, _c, _d;
455
+ return ({
456
+ active: parseFloat(lock.epochLockStart) < epoch &&
457
+ parseFloat(lock.epochLockEnd) >= epoch &&
458
+ userSnapshot !== undefined,
459
+ // No snapshot yet = the lock's opening shares: a lock made after
460
+ // clearing E starts at E but its first update is tagged E + 1, and
461
+ // reading that gap as 0 would book its principal as a top-up.
462
+ sharesAtEpoch: parseFloat((_b = (_a = sharesAsOf(lock.updates, epoch)) === null || _a === void 0 ? void 0 : _a.shares) !== null && _b !== void 0 ? _b : lock.initialTrancheShares),
463
+ sharesBeforeEpoch: parseFloat((_d = (_c = sharesAsOf(lock.updates, epoch - 1)) === null || _c === void 0 ? void 0 : _c.shares) !== null && _d !== void 0 ? _d : lock.initialTrancheShares),
464
+ });
465
+ });
466
+ return {
467
+ yield: epochTrancheYield({
468
+ epochInterest,
469
+ userShares,
470
+ trancheShares: trancheSharesAtInterest(series.trancheUpdates, series.exits, epoch, clearingStart),
471
+ trancheBalanceNow: series.trancheBalanceNow,
472
+ trancheSharesNow: series.trancheSharesNow,
473
+ locks,
474
+ }),
475
+ userShares,
476
+ };
477
+ }
478
+ /** A position's `PositionSeries` out of the fetched history series. */
479
+ export function positionSeriesFrom(rows, position) {
480
+ const trancheId = position.tranche.id;
481
+ return {
482
+ trancheBalanceNow: parseFloat(position.tranche.balance),
483
+ trancheSharesNow: parseFloat(position.tranche.shares),
484
+ userUpdates: rows.userShares.filter((row) => row.lendingPoolTrancheUserDetails.id === position.id),
485
+ trancheUpdates: rows.trancheShares.filter((row) => row.lendingPoolTranche.id === trancheId),
486
+ exits: rows.withdrawals.filter((row) => row.tranche.id === trancheId),
487
+ locks: position.userLendingPoolTrancheFixedTermDepositLocks.map((lock) => (Object.assign(Object.assign({}, lock), { updates: rows.lockShares.filter((row) => row.userLendingPoolTrancheFixedTermDepositLock.id ===
488
+ lock.id) }))),
489
+ };
490
+ }
491
+ /** `pool → epoch → clearing start`, from the clearing rows. */
492
+ export function clearingStarts(clearings) {
493
+ var _a;
494
+ const starts = new Map();
495
+ for (const clearing of clearings) {
496
+ const start = Number(clearing.startTimestamp);
497
+ // A clearing not yet started carries 0: no start to pair by.
498
+ if (!(start > 0))
499
+ continue;
500
+ const pool = clearing.lendingPool.id.toLowerCase();
501
+ const byEpoch = (_a = starts.get(pool)) !== null && _a !== void 0 ? _a : new Map();
502
+ byEpoch.set(Number(clearing.epochId), start);
503
+ starts.set(pool, byEpoch);
504
+ }
505
+ return starts;
506
+ }
289
507
  function groupBy(rows, key) {
290
508
  const map = new Map();
291
509
  for (const row of rows) {
@@ -302,12 +520,13 @@ function groupBy(rows, key) {
302
520
  * Pure: the fetched series → one entry per pool, tranche and settled epoch in
303
521
  * which the lender held shares (or a lock earned a top-up). Each entry runs the
304
522
  * same `epochTrancheYield` as the portfolio's last-epoch figure, with the same
305
- * share selection, so the newest entry equals that figure.
523
+ * share selection (`sharesAtInterest`, `trancheSharesAtInterest`), so the
524
+ * newest entry equals that figure.
306
525
  *
307
526
  * Sorted by `epochId`, then `poolId`, then `trancheId`.
308
527
  */
309
528
  export function buildYieldHistory(read, window, clock) {
310
- var _a, _b, _c, _d, _e, _f;
529
+ var _a, _b, _c, _d;
311
530
  const user = read.positions.user;
312
531
  if (!user)
313
532
  return [];
@@ -315,45 +534,36 @@ export function buildYieldHistory(read, window, clock) {
315
534
  const trancheSharesByTranche = groupBy(read.trancheShares, (row) => row.lendingPoolTranche.id);
316
535
  const userSharesByPosition = groupBy(read.userShares, (row) => row.lendingPoolTrancheUserDetails.id);
317
536
  const lockSharesByLock = groupBy(read.lockShares, (row) => row.userLendingPoolTrancheFixedTermDepositLock.id);
537
+ const withdrawalsByTranche = groupBy(read.withdrawals, (row) => row.tranche.id);
538
+ const starts = clearingStarts(read.clearings);
318
539
  const entries = [];
319
540
  for (const pool of user.lendingPoolUserDetails) {
541
+ const poolStarts = starts.get(pool.lendingPool.id.toLowerCase());
320
542
  for (const position of pool.lendingPoolTrancheUserDetails) {
321
543
  const trancheId = position.tranche.id;
322
544
  const trancheUpdates = (_a = trancheSharesByTranche.get(trancheId)) !== null && _a !== void 0 ? _a : [];
323
545
  const userUpdates = (_b = userSharesByPosition.get(position.id)) !== null && _b !== void 0 ? _b : [];
546
+ const trancheWithdrawals = (_c = withdrawalsByTranche.get(trancheId)) !== null && _c !== void 0 ? _c : [];
324
547
  const trancheBalanceNow = parseFloat(position.tranche.balance);
325
548
  const trancheSharesNow = parseFloat(position.tranche.shares);
326
- for (const record of (_c = interestByTranche.get(trancheId)) !== null && _c !== void 0 ? _c : []) {
549
+ for (const record of (_d = interestByTranche.get(trancheId)) !== null && _d !== void 0 ? _d : []) {
327
550
  const epoch = Number(record.epochId);
328
551
  if (epoch < window.fromEpoch || epoch > window.toEpoch) {
329
552
  continue;
330
553
  }
331
- const userSnapshot = sharesAsOf(userUpdates, epoch);
332
- const userShares = parseFloat((_d = userSnapshot === null || userSnapshot === void 0 ? void 0 : userSnapshot.shares) !== null && _d !== void 0 ? _d : '0');
333
- const locks = position.userLendingPoolTrancheFixedTermDepositLocks.map((lock) => {
334
- var _a, _b, _c, _d, _e;
335
- const lockUpdates = (_a = lockSharesByLock.get(lock.id)) !== null && _a !== void 0 ? _a : [];
336
- return {
337
- active: parseFloat(lock.epochLockStart) < epoch &&
338
- parseFloat(lock.epochLockEnd) >= epoch &&
339
- userSnapshot !== undefined,
340
- // No snapshot yet = the lock's opening
341
- // shares: a lock made after clearing E
342
- // starts at E but its first update is
343
- // tagged E + 1, and reading that gap as 0
344
- // would book its principal as a top-up.
345
- sharesAtEpoch: parseFloat((_c = (_b = sharesAsOf(lockUpdates, epoch)) === null || _b === void 0 ? void 0 : _b.shares) !== null && _c !== void 0 ? _c : lock.initialTrancheShares),
346
- sharesBeforeEpoch: parseFloat((_e = (_d = sharesAsOf(lockUpdates, epoch - 1)) === null || _d === void 0 ? void 0 : _d.shares) !== null && _e !== void 0 ? _e : lock.initialTrancheShares),
347
- };
348
- });
349
- const { total } = epochTrancheYield({
350
- epochInterest: parseFloat(record.epochInterestAmount),
351
- userShares,
352
- trancheShares: parseFloat((_f = (_e = sharesAsOf(trancheUpdates, epoch)) === null || _e === void 0 ? void 0 : _e.shares) !== null && _f !== void 0 ? _f : '0'),
554
+ const clearingStart = poolStarts === null || poolStarts === void 0 ? void 0 : poolStarts.get(epoch);
555
+ const { yield: epochYield, userShares } = positionEpochYield({
353
556
  trancheBalanceNow,
354
557
  trancheSharesNow,
355
- locks,
356
- });
558
+ userUpdates,
559
+ trancheUpdates,
560
+ exits: trancheWithdrawals,
561
+ locks: position.userLendingPoolTrancheFixedTermDepositLocks.map((lock) => {
562
+ var _a;
563
+ return (Object.assign(Object.assign({}, lock), { updates: (_a = lockSharesByLock.get(lock.id)) !== null && _a !== void 0 ? _a : [] }));
564
+ }),
565
+ }, epoch, parseFloat(record.epochInterestAmount), clearingStart);
566
+ const total = epochYield.total;
357
567
  // Nothing held and nothing topped up: not part of this
358
568
  // lender's history (a new lender's history is empty).
359
569
  if (userShares <= 0 && total === 0)