@kasufinance/kasu-sdk 2.14.0 → 2.16.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 +45 -5
- package/dist/bundle.cjs.js +1484 -228
- package/dist/bundle.esm.js +1476 -229
- package/dist/domain/index.d.ts +6 -1
- 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.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/domain/yield-history.d.ts +25 -1
- package/dist/domain/yield-history.js +41 -11
- package/dist/domain/yield-history.js.map +1 -1
- package/dist/facade/index.d.ts +1 -1
- package/dist/facade/index.js +2 -0
- package/dist/facade/index.js.map +1 -1
- package/dist/facade/strategies.d.ts +85 -3
- package/dist/facade/strategies.js +182 -6
- package/dist/facade/strategies.js.map +1 -1
- 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.js.map +1 -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.js +89 -0
- package/dist/services/DataService/tvl-history.js.map +1 -1
- package/dist/services/Portfolio/portfolio.d.ts +54 -1
- package/dist/services/Portfolio/portfolio.js +357 -80
- package/dist/services/Portfolio/portfolio.js.map +1 -1
- package/dist/services/Portfolio/queries.d.ts +16 -0
- package/dist/services/Portfolio/queries.js +119 -2
- package/dist/services/Portfolio/queries.js.map +1 -1
- package/dist/services/Portfolio/types.d.ts +94 -6
- package/dist/services/Portfolio/yield-history.d.ts +148 -10
- package/dist/services/Portfolio/yield-history.js +241 -31
- package/dist/services/Portfolio/yield-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.d.ts +16 -0
- package/dist/services/UserLending/user-lending.js +96 -16
- package/dist/services/UserLending/user-lending.js.map +1 -1
- package/dist/services/shared.d.ts +7 -0
- package/dist/services/shared.js +48 -0
- package/dist/services/shared.js.map +1 -1
- package/package.json +1 -1
- package/src/domain/index.ts +27 -1
- package/src/domain/loan-contract.ts +16 -5
- package/src/domain/strategy-details.ts +528 -0
- package/src/domain/yield-history.ts +47 -11
- package/src/facade/index.ts +3 -0
- package/src/facade/strategies.ts +205 -10
- package/src/facade/types.ts +85 -0
- package/src/flows/deposit-flow.ts +19 -5
- package/src/services/Portfolio/portfolio.ts +559 -126
- package/src/services/Portfolio/queries.ts +119 -2
- package/src/services/Portfolio/types.ts +91 -6
- package/src/services/Portfolio/yield-history.ts +405 -56
- package/src/services/UserLending/user-lending.ts +46 -0
- package/src/services/shared.ts +48 -0
|
@@ -2,6 +2,7 @@ import { gql } from 'graphql-request';
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
EpochLockShares,
|
|
5
|
+
EpochTrancheYield,
|
|
5
6
|
epochTrancheYield,
|
|
6
7
|
YieldHistoryEntry,
|
|
7
8
|
} from '../../domain/yield-history';
|
|
@@ -108,6 +109,7 @@ export const yieldHistoryPositionsQuery = gql`
|
|
|
108
109
|
}
|
|
109
110
|
lendingPoolTrancheUserDetails(first: $first) {
|
|
110
111
|
id
|
|
112
|
+
shares
|
|
111
113
|
tranche {
|
|
112
114
|
id
|
|
113
115
|
balance
|
|
@@ -122,33 +124,51 @@ export const yieldHistoryPositionsQuery = gql`
|
|
|
122
124
|
}
|
|
123
125
|
}
|
|
124
126
|
}
|
|
127
|
+
_meta {
|
|
128
|
+
block {
|
|
129
|
+
number
|
|
130
|
+
}
|
|
131
|
+
}
|
|
125
132
|
}
|
|
126
133
|
`;
|
|
127
134
|
|
|
128
135
|
/**
|
|
129
|
-
* Round trip 2 onwards: the
|
|
136
|
+
* Round trip 2 onwards: the six per-epoch series, one page of each per
|
|
130
137
|
* request, cursor-paginated by `id`. A series that is exhausted is skipped with
|
|
131
138
|
* `@include`, so a history costs as many requests as its LONGEST series has
|
|
132
139
|
* pages — normally one — never one per epoch.
|
|
133
140
|
*
|
|
134
141
|
* Share snapshots are read up to the window's end with no lower bound: the
|
|
135
|
-
* value AS OF the first epoch may come from an update long before it.
|
|
142
|
+
* value AS OF the first epoch may come from an update long before it. Each
|
|
143
|
+
* carries its `timestamp`, and the window's clearings their start, because a
|
|
144
|
+
* cycle's interest is paired with the shares in force when the clearing
|
|
145
|
+
* APPLIED it (`sharesAtInterest`), which the epoch tag alone gets wrong for a
|
|
146
|
+
* withdrawal accepted between clearings — so the lender's updates are read one
|
|
147
|
+
* tag past the window (`$userSharesToEpoch`), where the subgraph puts it.
|
|
148
|
+
* Every share exit between clearings is read too (`trancheSharesAtInterest`
|
|
149
|
+
* rebuilds the tranche total from the previous clearing's snapshot).
|
|
136
150
|
*/
|
|
137
151
|
export const yieldHistoryPageQuery = gql`
|
|
138
152
|
query YieldHistoryPage(
|
|
153
|
+
$pools: [String!]!
|
|
139
154
|
$tranches: [String!]!
|
|
140
155
|
$userTranches: [String!]!
|
|
141
156
|
$locks: [String!]!
|
|
142
157
|
$fromEpoch: BigInt!
|
|
143
158
|
$toEpoch: BigInt!
|
|
159
|
+
$userSharesToEpoch: BigInt!
|
|
144
160
|
$interestCursor: ID!
|
|
145
161
|
$trancheSharesCursor: ID!
|
|
146
162
|
$userSharesCursor: ID!
|
|
147
163
|
$lockSharesCursor: ID!
|
|
164
|
+
$clearingsCursor: ID!
|
|
165
|
+
$withdrawalsCursor: ID!
|
|
148
166
|
$withInterest: Boolean!
|
|
149
167
|
$withTrancheShares: Boolean!
|
|
150
168
|
$withUserShares: Boolean!
|
|
151
169
|
$withLockShares: Boolean!
|
|
170
|
+
$withClearings: Boolean!
|
|
171
|
+
$withWithdrawals: Boolean!
|
|
152
172
|
$first: Int!
|
|
153
173
|
) {
|
|
154
174
|
interest: lendingPoolTrancheEpochInterests(
|
|
@@ -183,6 +203,7 @@ export const yieldHistoryPageQuery = gql`
|
|
|
183
203
|
epochId
|
|
184
204
|
shareUpdatesIndex
|
|
185
205
|
shares
|
|
206
|
+
timestamp
|
|
186
207
|
lendingPoolTranche {
|
|
187
208
|
id
|
|
188
209
|
}
|
|
@@ -194,13 +215,14 @@ export const yieldHistoryPageQuery = gql`
|
|
|
194
215
|
where: {
|
|
195
216
|
id_gt: $userSharesCursor
|
|
196
217
|
lendingPoolTrancheUserDetails_in: $userTranches
|
|
197
|
-
epochId_lte: $
|
|
218
|
+
epochId_lte: $userSharesToEpoch
|
|
198
219
|
}
|
|
199
220
|
) @include(if: $withUserShares) {
|
|
200
221
|
id
|
|
201
222
|
epochId
|
|
202
223
|
shareUpdatesIndex
|
|
203
224
|
shares
|
|
225
|
+
timestamp
|
|
204
226
|
lendingPoolTrancheUserDetails {
|
|
205
227
|
id
|
|
206
228
|
}
|
|
@@ -223,6 +245,45 @@ export const yieldHistoryPageQuery = gql`
|
|
|
223
245
|
id
|
|
224
246
|
}
|
|
225
247
|
}
|
|
248
|
+
clearings: lendingPoolClearings(
|
|
249
|
+
first: $first
|
|
250
|
+
orderBy: id
|
|
251
|
+
orderDirection: asc
|
|
252
|
+
where: {
|
|
253
|
+
id_gt: $clearingsCursor
|
|
254
|
+
lendingPool_in: $pools
|
|
255
|
+
epochId_gte: $fromEpoch
|
|
256
|
+
epochId_lte: $toEpoch
|
|
257
|
+
}
|
|
258
|
+
) @include(if: $withClearings) {
|
|
259
|
+
id
|
|
260
|
+
epochId
|
|
261
|
+
startTimestamp
|
|
262
|
+
lendingPool {
|
|
263
|
+
id
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
withdrawals: userRequestEvents(
|
|
267
|
+
first: $first
|
|
268
|
+
orderBy: id
|
|
269
|
+
orderDirection: asc
|
|
270
|
+
where: {
|
|
271
|
+
id_gt: $withdrawalsCursor
|
|
272
|
+
type_in: [WithdrawalAccepted, ImmediateWithdrawalExecuted]
|
|
273
|
+
tranche_in: $tranches
|
|
274
|
+
epochId_gte: $fromEpoch
|
|
275
|
+
epochId_lte: $toEpoch
|
|
276
|
+
}
|
|
277
|
+
) @include(if: $withWithdrawals) {
|
|
278
|
+
id
|
|
279
|
+
type
|
|
280
|
+
epochId
|
|
281
|
+
createdOn
|
|
282
|
+
sharesAmount
|
|
283
|
+
tranche {
|
|
284
|
+
id
|
|
285
|
+
}
|
|
286
|
+
}
|
|
226
287
|
}
|
|
227
288
|
`;
|
|
228
289
|
|
|
@@ -232,6 +293,8 @@ export interface YieldHistoryPositionsResult {
|
|
|
232
293
|
lendingPool: { id: string };
|
|
233
294
|
lendingPoolTrancheUserDetails: {
|
|
234
295
|
id: string;
|
|
296
|
+
/** The ledger's share count (accepted deposits − withdrawals ± top-ups). */
|
|
297
|
+
shares?: string;
|
|
235
298
|
tranche: { id: string; balance: string; shares: string };
|
|
236
299
|
userLendingPoolTrancheFixedTermDepositLocks: {
|
|
237
300
|
id: string;
|
|
@@ -242,6 +305,8 @@ export interface YieldHistoryPositionsResult {
|
|
|
242
305
|
}[];
|
|
243
306
|
}[];
|
|
244
307
|
} | null;
|
|
308
|
+
/** The block the subgraph answered at. */
|
|
309
|
+
_meta?: { block: { number: number } };
|
|
245
310
|
}
|
|
246
311
|
|
|
247
312
|
/** One share snapshot: the value from `epochId` on, until the next one. */
|
|
@@ -252,6 +317,18 @@ export interface ShareUpdateRow {
|
|
|
252
317
|
shares: string;
|
|
253
318
|
}
|
|
254
319
|
|
|
320
|
+
/** A tranche or lender share snapshot, with the block time it was written at. */
|
|
321
|
+
export interface TimedShareUpdateRow extends ShareUpdateRow {
|
|
322
|
+
/** Unix seconds. */
|
|
323
|
+
timestamp: string;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** What the share selection reads from a snapshot — no entity id needed. */
|
|
327
|
+
export type TimedShareSnapshot = Pick<
|
|
328
|
+
TimedShareUpdateRow,
|
|
329
|
+
'epochId' | 'shareUpdatesIndex' | 'shares' | 'timestamp'
|
|
330
|
+
>;
|
|
331
|
+
|
|
255
332
|
export interface InterestRow {
|
|
256
333
|
id: string;
|
|
257
334
|
epochId: string;
|
|
@@ -259,28 +336,60 @@ export interface InterestRow {
|
|
|
259
336
|
lendingPoolTranche: { id: string };
|
|
260
337
|
}
|
|
261
338
|
|
|
339
|
+
/** A pool's clearing for one epoch: its first transaction applied the interest. */
|
|
340
|
+
export interface ClearingRow {
|
|
341
|
+
id: string;
|
|
342
|
+
epochId: string;
|
|
343
|
+
/** Unix seconds of the clearing's first transaction. */
|
|
344
|
+
startTimestamp: string;
|
|
345
|
+
lendingPool: { id: string };
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Shares leaving a tranche (any lender): an accepted withdrawal or an
|
|
350
|
+
* immediate one. Each carries the pool's clearing epoch at the time
|
|
351
|
+
* (`epochClearingId`) — so one between clearings E − 1 and E is tagged E.
|
|
352
|
+
*/
|
|
353
|
+
export interface ShareExitRow {
|
|
354
|
+
id: string;
|
|
355
|
+
type: 'WithdrawalAccepted' | 'ImmediateWithdrawalExecuted';
|
|
356
|
+
epochId: string;
|
|
357
|
+
/** Unix seconds. */
|
|
358
|
+
createdOn: string;
|
|
359
|
+
sharesAmount: string | null;
|
|
360
|
+
tranche: { id: string };
|
|
361
|
+
}
|
|
362
|
+
|
|
262
363
|
export interface YieldHistoryPageResult {
|
|
263
364
|
interest?: InterestRow[];
|
|
264
|
-
trancheShares?: (
|
|
265
|
-
|
|
365
|
+
trancheShares?: (TimedShareUpdateRow & {
|
|
366
|
+
lendingPoolTranche: { id: string };
|
|
367
|
+
})[];
|
|
368
|
+
userShares?: (TimedShareUpdateRow & {
|
|
266
369
|
lendingPoolTrancheUserDetails: { id: string };
|
|
267
370
|
})[];
|
|
268
371
|
lockShares?: (ShareUpdateRow & {
|
|
269
372
|
userLendingPoolTrancheFixedTermDepositLock: { id: string };
|
|
270
373
|
})[];
|
|
374
|
+
clearings?: ClearingRow[];
|
|
375
|
+
withdrawals?: ShareExitRow[];
|
|
271
376
|
}
|
|
272
377
|
|
|
273
378
|
/** Everything `buildYieldHistory` reads, keyed by the subgraph's entity ids. */
|
|
274
379
|
export interface YieldHistoryRead {
|
|
275
380
|
positions: YieldHistoryPositionsResult;
|
|
276
381
|
interest: InterestRow[];
|
|
277
|
-
trancheShares: (
|
|
278
|
-
|
|
382
|
+
trancheShares: (TimedShareUpdateRow & {
|
|
383
|
+
lendingPoolTranche: { id: string };
|
|
384
|
+
})[];
|
|
385
|
+
userShares: (TimedShareUpdateRow & {
|
|
279
386
|
lendingPoolTrancheUserDetails: { id: string };
|
|
280
387
|
})[];
|
|
281
388
|
lockShares: (ShareUpdateRow & {
|
|
282
389
|
userLendingPoolTrancheFixedTermDepositLock: { id: string };
|
|
283
390
|
})[];
|
|
391
|
+
clearings: ClearingRow[];
|
|
392
|
+
withdrawals: ShareExitRow[];
|
|
284
393
|
}
|
|
285
394
|
|
|
286
395
|
/** The chain's epoch clock: epoch E ends at `epochZeroStart + (E + 1) × duration`. */
|
|
@@ -294,12 +403,17 @@ type PageRequest = (
|
|
|
294
403
|
) => Promise<YieldHistoryPageResult>;
|
|
295
404
|
|
|
296
405
|
/**
|
|
297
|
-
* Fetch the
|
|
406
|
+
* Fetch the six series for the window, paginating each by `id` until a short
|
|
298
407
|
* page. Returns the rows and how many requests it took.
|
|
299
408
|
*/
|
|
300
409
|
export async function fetchYieldHistorySeries(
|
|
301
410
|
request: PageRequest,
|
|
302
|
-
ids: {
|
|
411
|
+
ids: {
|
|
412
|
+
pools: string[];
|
|
413
|
+
tranches: string[];
|
|
414
|
+
userTranches: string[];
|
|
415
|
+
locks: string[];
|
|
416
|
+
},
|
|
303
417
|
window: YieldHistoryWindow,
|
|
304
418
|
): Promise<{
|
|
305
419
|
rows: Omit<YieldHistoryRead, 'positions'>;
|
|
@@ -310,6 +424,8 @@ export async function fetchYieldHistorySeries(
|
|
|
310
424
|
trancheShares: [],
|
|
311
425
|
userShares: [],
|
|
312
426
|
lockShares: [],
|
|
427
|
+
clearings: [],
|
|
428
|
+
withdrawals: [],
|
|
313
429
|
};
|
|
314
430
|
const series = [
|
|
315
431
|
{ key: 'interest', flag: 'withInterest', cursor: 'interestCursor' },
|
|
@@ -328,6 +444,16 @@ export async function fetchYieldHistorySeries(
|
|
|
328
444
|
flag: 'withLockShares',
|
|
329
445
|
cursor: 'lockSharesCursor',
|
|
330
446
|
},
|
|
447
|
+
{
|
|
448
|
+
key: 'clearings',
|
|
449
|
+
flag: 'withClearings',
|
|
450
|
+
cursor: 'clearingsCursor',
|
|
451
|
+
},
|
|
452
|
+
{
|
|
453
|
+
key: 'withdrawals',
|
|
454
|
+
flag: 'withWithdrawals',
|
|
455
|
+
cursor: 'withdrawalsCursor',
|
|
456
|
+
},
|
|
331
457
|
] as const;
|
|
332
458
|
|
|
333
459
|
const cursors: Record<(typeof series)[number]['key'], string> = {
|
|
@@ -335,12 +461,16 @@ export async function fetchYieldHistorySeries(
|
|
|
335
461
|
trancheShares: '',
|
|
336
462
|
userShares: '',
|
|
337
463
|
lockShares: '',
|
|
464
|
+
clearings: '',
|
|
465
|
+
withdrawals: '',
|
|
338
466
|
};
|
|
339
467
|
const open: Record<(typeof series)[number]['key'], boolean> = {
|
|
340
468
|
interest: ids.tranches.length > 0,
|
|
341
469
|
trancheShares: ids.tranches.length > 0,
|
|
342
470
|
userShares: ids.userTranches.length > 0,
|
|
343
471
|
lockShares: ids.locks.length > 0,
|
|
472
|
+
clearings: ids.pools.length > 0,
|
|
473
|
+
withdrawals: ids.tranches.length > 0,
|
|
344
474
|
};
|
|
345
475
|
|
|
346
476
|
let requests = 0;
|
|
@@ -352,11 +482,15 @@ export async function fetchYieldHistorySeries(
|
|
|
352
482
|
}
|
|
353
483
|
|
|
354
484
|
const variables: Record<string, unknown> = {
|
|
485
|
+
pools: ids.pools,
|
|
355
486
|
tranches: ids.tranches,
|
|
356
487
|
userTranches: ids.userTranches,
|
|
357
488
|
locks: ids.locks,
|
|
358
489
|
fromEpoch: String(window.fromEpoch),
|
|
359
490
|
toEpoch: String(window.toEpoch),
|
|
491
|
+
// One tag further for the lender: a withdrawal accepted between
|
|
492
|
+
// clearings is tagged with the epoch AFTER the one it counts in.
|
|
493
|
+
userSharesToEpoch: String(window.toEpoch + 1),
|
|
360
494
|
first: YIELD_HISTORY_PAGE_SIZE,
|
|
361
495
|
};
|
|
362
496
|
for (const { key, flag, cursor } of series) {
|
|
@@ -394,11 +528,10 @@ export async function fetchYieldHistorySeries(
|
|
|
394
528
|
* `lendingPortfolioQuery` selects with `orderBy: shareUpdatesIndex, desc,
|
|
395
529
|
* first: 1, where: { epochId_lt: epoch + 1 }`. `undefined` when none.
|
|
396
530
|
*/
|
|
397
|
-
export function sharesAsOf
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
let found: ShareUpdateRow | undefined;
|
|
531
|
+
export function sharesAsOf<
|
|
532
|
+
T extends Pick<ShareUpdateRow, 'epochId' | 'shareUpdatesIndex' | 'shares'>,
|
|
533
|
+
>(updates: readonly T[], epoch: number): T | undefined {
|
|
534
|
+
let found: T | undefined;
|
|
402
535
|
for (const update of updates) {
|
|
403
536
|
if (Number(update.epochId) > epoch) continue;
|
|
404
537
|
if (!found || update.shareUpdatesIndex > found.shareUpdatesIndex) {
|
|
@@ -408,6 +541,233 @@ export function sharesAsOf(
|
|
|
408
541
|
return found;
|
|
409
542
|
}
|
|
410
543
|
|
|
544
|
+
/**
|
|
545
|
+
* The snapshot in force when the clearing of `epoch` APPLIED that epoch's
|
|
546
|
+
* interest — the shares the interest was actually spread over.
|
|
547
|
+
*
|
|
548
|
+
* A clearing applies interest FIRST (`ClearingCoordinator.doClearing`: the
|
|
549
|
+
* `applyInterests` call before steps 1–5), then accepts deposits and
|
|
550
|
+
* withdrawals. So an update is in force for epoch E's interest when it is
|
|
551
|
+
* tagged `≤ E` (the subgraph tags every update written during clearing E, or
|
|
552
|
+
* after it, `E + 1` or later), OR when it was written before clearing E began
|
|
553
|
+
* (`timestamp < clearingStart`). The second test is the one that matters: a
|
|
554
|
+
* withdrawal the pool manager accepts BETWEEN clearings
|
|
555
|
+
* (`forceAcceptWithdrawalRequest`) is tagged `E + 1` by the subgraph, as if a
|
|
556
|
+
* clearing had accepted it, yet the shares were gone before clearing E.
|
|
557
|
+
*
|
|
558
|
+
* Without a clearing start (none recorded) it is `sharesAsOf` — the tag alone.
|
|
559
|
+
*/
|
|
560
|
+
export function sharesAtInterest<T extends TimedShareSnapshot>(
|
|
561
|
+
updates: readonly T[],
|
|
562
|
+
epoch: number,
|
|
563
|
+
clearingStart: number | undefined,
|
|
564
|
+
): T | undefined {
|
|
565
|
+
let found: T | undefined;
|
|
566
|
+
for (const update of updates) {
|
|
567
|
+
const inForce =
|
|
568
|
+
Number(update.epochId) <= epoch ||
|
|
569
|
+
(clearingStart !== undefined &&
|
|
570
|
+
Number(update.timestamp) < clearingStart);
|
|
571
|
+
if (!inForce) continue;
|
|
572
|
+
if (!found || update.shareUpdatesIndex > found.shareUpdatesIndex) {
|
|
573
|
+
found = update;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
return found;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* The tranche's total shares when the clearing of `epoch` applied its
|
|
581
|
+
* interest.
|
|
582
|
+
*
|
|
583
|
+
* The subgraph writes a tranche snapshot when a clearing ENDS (tagged with
|
|
584
|
+
* the next epoch) and at each immediate withdrawal, but none when the manager
|
|
585
|
+
* accepts a withdrawal between clearings — so the newest snapshot can predate
|
|
586
|
+
* shares that had already left. Instead of guessing the order of events in a
|
|
587
|
+
* block, the total is rebuilt from the one snapshot whose place is certain:
|
|
588
|
+
* the FIRST one tagged `epoch`, written when clearing `epoch − 1` ended (the
|
|
589
|
+
* tag itself starts there). Every share exit between that and clearing
|
|
590
|
+
* `epoch` is tagged `epoch` (the pool's clearing epoch at the time), and none
|
|
591
|
+
* can happen during a clearing (`notPendingClearing`,
|
|
592
|
+
* `verifyClearingNotPending`), while deposits and top-ups only happen inside
|
|
593
|
+
* one. So: that snapshot less every exit tagged `epoch` before the clearing's
|
|
594
|
+
* start (the clearing's own acceptances come at or after it).
|
|
595
|
+
*
|
|
596
|
+
* Without a clearing start, or without a snapshot tagged `epoch` (a tranche's
|
|
597
|
+
* first clearing), it is the snapshot in force by tag alone.
|
|
598
|
+
*/
|
|
599
|
+
export function trancheSharesAtInterest(
|
|
600
|
+
snapshots: readonly TimedShareSnapshot[],
|
|
601
|
+
exits: readonly Pick<
|
|
602
|
+
ShareExitRow,
|
|
603
|
+
'epochId' | 'createdOn' | 'sharesAmount'
|
|
604
|
+
>[],
|
|
605
|
+
epoch: number,
|
|
606
|
+
clearingStart: number | undefined,
|
|
607
|
+
): number {
|
|
608
|
+
let base: TimedShareSnapshot | undefined;
|
|
609
|
+
if (clearingStart !== undefined) {
|
|
610
|
+
for (const snapshot of snapshots) {
|
|
611
|
+
if (Number(snapshot.epochId) !== epoch) continue;
|
|
612
|
+
if (!base || snapshot.shareUpdatesIndex < base.shareUpdatesIndex) {
|
|
613
|
+
base = snapshot;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
if (clearingStart === undefined || !base) {
|
|
618
|
+
return parseFloat(sharesAsOf(snapshots, epoch)?.shares ?? '0');
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
let shares = parseFloat(base.shares);
|
|
622
|
+
for (const exit of exits) {
|
|
623
|
+
if (
|
|
624
|
+
Number(exit.epochId) === epoch &&
|
|
625
|
+
Number(exit.createdOn) < clearingStart
|
|
626
|
+
) {
|
|
627
|
+
shares -= parseFloat(exit.sharesAmount ?? '0');
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
return shares;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/** A fixed-term lock inside one lender's tranche position. */
|
|
634
|
+
export interface PositionLock {
|
|
635
|
+
epochLockStart: string;
|
|
636
|
+
epochLockEnd: string;
|
|
637
|
+
initialTrancheShares: string;
|
|
638
|
+
/** The lock's share snapshots (paired by tag: its tags are right). */
|
|
639
|
+
updates: readonly Pick<
|
|
640
|
+
ShareUpdateRow,
|
|
641
|
+
'epochId' | 'shareUpdatesIndex' | 'shares'
|
|
642
|
+
>[];
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/** Everything one lender's tranche position needs to price one epoch. */
|
|
646
|
+
export interface PositionSeries {
|
|
647
|
+
trancheBalanceNow: number;
|
|
648
|
+
trancheSharesNow: number;
|
|
649
|
+
userUpdates: readonly TimedShareSnapshot[];
|
|
650
|
+
trancheUpdates: readonly TimedShareSnapshot[];
|
|
651
|
+
exits: readonly Pick<
|
|
652
|
+
ShareExitRow,
|
|
653
|
+
'epochId' | 'createdOn' | 'sharesAmount'
|
|
654
|
+
>[];
|
|
655
|
+
locks: readonly PositionLock[];
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* THE per-position, per-epoch selection both `getYieldHistory` and
|
|
660
|
+
* `getPortfolioLendingData` run: the shares in force when clearing `epoch`
|
|
661
|
+
* applied its interest (`sharesAtInterest`, `trancheSharesAtInterest`), each
|
|
662
|
+
* lock's snapshots as of `epoch` and `epoch − 1`, then `epochTrancheYield`.
|
|
663
|
+
*/
|
|
664
|
+
export function positionEpochYield(
|
|
665
|
+
series: PositionSeries,
|
|
666
|
+
epoch: number,
|
|
667
|
+
epochInterest: number,
|
|
668
|
+
clearingStart: number | undefined,
|
|
669
|
+
): { yield: EpochTrancheYield; userShares: number } {
|
|
670
|
+
const userSnapshot = sharesAtInterest(
|
|
671
|
+
series.userUpdates,
|
|
672
|
+
epoch,
|
|
673
|
+
clearingStart,
|
|
674
|
+
);
|
|
675
|
+
const userShares = parseFloat(userSnapshot?.shares ?? '0');
|
|
676
|
+
|
|
677
|
+
const locks: EpochLockShares[] = series.locks.map((lock) => ({
|
|
678
|
+
active:
|
|
679
|
+
parseFloat(lock.epochLockStart) < epoch &&
|
|
680
|
+
parseFloat(lock.epochLockEnd) >= epoch &&
|
|
681
|
+
userSnapshot !== undefined,
|
|
682
|
+
// No snapshot yet = the lock's opening shares: a lock made after
|
|
683
|
+
// clearing E starts at E but its first update is tagged E + 1, and
|
|
684
|
+
// reading that gap as 0 would book its principal as a top-up.
|
|
685
|
+
sharesAtEpoch: parseFloat(
|
|
686
|
+
sharesAsOf(lock.updates, epoch)?.shares ??
|
|
687
|
+
lock.initialTrancheShares,
|
|
688
|
+
),
|
|
689
|
+
sharesBeforeEpoch: parseFloat(
|
|
690
|
+
sharesAsOf(lock.updates, epoch - 1)?.shares ??
|
|
691
|
+
lock.initialTrancheShares,
|
|
692
|
+
),
|
|
693
|
+
}));
|
|
694
|
+
|
|
695
|
+
return {
|
|
696
|
+
yield: epochTrancheYield({
|
|
697
|
+
epochInterest,
|
|
698
|
+
userShares,
|
|
699
|
+
trancheShares: trancheSharesAtInterest(
|
|
700
|
+
series.trancheUpdates,
|
|
701
|
+
series.exits,
|
|
702
|
+
epoch,
|
|
703
|
+
clearingStart,
|
|
704
|
+
),
|
|
705
|
+
trancheBalanceNow: series.trancheBalanceNow,
|
|
706
|
+
trancheSharesNow: series.trancheSharesNow,
|
|
707
|
+
locks,
|
|
708
|
+
}),
|
|
709
|
+
userShares,
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/** One lender's tranche position, as the positions and portfolio queries read it. */
|
|
714
|
+
export interface PositionRef {
|
|
715
|
+
id: string;
|
|
716
|
+
tranche: { id: string; balance: string; shares: string };
|
|
717
|
+
userLendingPoolTrancheFixedTermDepositLocks: {
|
|
718
|
+
id?: string;
|
|
719
|
+
epochLockStart: string;
|
|
720
|
+
epochLockEnd: string;
|
|
721
|
+
initialTrancheShares: string;
|
|
722
|
+
}[];
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/** A position's `PositionSeries` out of the fetched history series. */
|
|
726
|
+
export function positionSeriesFrom(
|
|
727
|
+
rows: Omit<YieldHistoryRead, 'positions'>,
|
|
728
|
+
position: PositionRef,
|
|
729
|
+
): PositionSeries {
|
|
730
|
+
const trancheId = position.tranche.id;
|
|
731
|
+
return {
|
|
732
|
+
trancheBalanceNow: parseFloat(position.tranche.balance),
|
|
733
|
+
trancheSharesNow: parseFloat(position.tranche.shares),
|
|
734
|
+
userUpdates: rows.userShares.filter(
|
|
735
|
+
(row) => row.lendingPoolTrancheUserDetails.id === position.id,
|
|
736
|
+
),
|
|
737
|
+
trancheUpdates: rows.trancheShares.filter(
|
|
738
|
+
(row) => row.lendingPoolTranche.id === trancheId,
|
|
739
|
+
),
|
|
740
|
+
exits: rows.withdrawals.filter((row) => row.tranche.id === trancheId),
|
|
741
|
+
locks: position.userLendingPoolTrancheFixedTermDepositLocks.map(
|
|
742
|
+
(lock) => ({
|
|
743
|
+
...lock,
|
|
744
|
+
updates: rows.lockShares.filter(
|
|
745
|
+
(row) =>
|
|
746
|
+
row.userLendingPoolTrancheFixedTermDepositLock.id ===
|
|
747
|
+
lock.id,
|
|
748
|
+
),
|
|
749
|
+
}),
|
|
750
|
+
),
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/** `pool → epoch → clearing start`, from the clearing rows. */
|
|
755
|
+
export function clearingStarts(
|
|
756
|
+
clearings: readonly ClearingRow[],
|
|
757
|
+
): Map<string, Map<number, number>> {
|
|
758
|
+
const starts = new Map<string, Map<number, number>>();
|
|
759
|
+
for (const clearing of clearings) {
|
|
760
|
+
const start = Number(clearing.startTimestamp);
|
|
761
|
+
// A clearing not yet started carries 0: no start to pair by.
|
|
762
|
+
if (!(start > 0)) continue;
|
|
763
|
+
const pool = clearing.lendingPool.id.toLowerCase();
|
|
764
|
+
const byEpoch = starts.get(pool) ?? new Map<number, number>();
|
|
765
|
+
byEpoch.set(Number(clearing.epochId), start);
|
|
766
|
+
starts.set(pool, byEpoch);
|
|
767
|
+
}
|
|
768
|
+
return starts;
|
|
769
|
+
}
|
|
770
|
+
|
|
411
771
|
function groupBy<T>(
|
|
412
772
|
rows: readonly T[],
|
|
413
773
|
key: (row: T) => string,
|
|
@@ -426,7 +786,8 @@ function groupBy<T>(
|
|
|
426
786
|
* Pure: the fetched series → one entry per pool, tranche and settled epoch in
|
|
427
787
|
* which the lender held shares (or a lock earned a top-up). Each entry runs the
|
|
428
788
|
* same `epochTrancheYield` as the portfolio's last-epoch figure, with the same
|
|
429
|
-
* share selection, so the
|
|
789
|
+
* share selection (`sharesAtInterest`, `trancheSharesAtInterest`), so the
|
|
790
|
+
* newest entry equals that figure.
|
|
430
791
|
*
|
|
431
792
|
* Sorted by `epochId`, then `poolId`, then `trancheId`.
|
|
432
793
|
*/
|
|
@@ -454,14 +815,22 @@ export function buildYieldHistory(
|
|
|
454
815
|
read.lockShares,
|
|
455
816
|
(row) => row.userLendingPoolTrancheFixedTermDepositLock.id,
|
|
456
817
|
);
|
|
818
|
+
const withdrawalsByTranche = groupBy(
|
|
819
|
+
read.withdrawals,
|
|
820
|
+
(row) => row.tranche.id,
|
|
821
|
+
);
|
|
822
|
+
const starts = clearingStarts(read.clearings);
|
|
457
823
|
|
|
458
824
|
const entries: YieldHistoryEntry[] = [];
|
|
459
825
|
|
|
460
826
|
for (const pool of user.lendingPoolUserDetails) {
|
|
827
|
+
const poolStarts = starts.get(pool.lendingPool.id.toLowerCase());
|
|
461
828
|
for (const position of pool.lendingPoolTrancheUserDetails) {
|
|
462
829
|
const trancheId = position.tranche.id;
|
|
463
830
|
const trancheUpdates = trancheSharesByTranche.get(trancheId) ?? [];
|
|
464
831
|
const userUpdates = userSharesByPosition.get(position.id) ?? [];
|
|
832
|
+
const trancheWithdrawals =
|
|
833
|
+
withdrawalsByTranche.get(trancheId) ?? [];
|
|
465
834
|
const trancheBalanceNow = parseFloat(position.tranche.balance);
|
|
466
835
|
const trancheSharesNow = parseFloat(position.tranche.shares);
|
|
467
836
|
|
|
@@ -470,47 +839,27 @@ export function buildYieldHistory(
|
|
|
470
839
|
if (epoch < window.fromEpoch || epoch > window.toEpoch) {
|
|
471
840
|
continue;
|
|
472
841
|
}
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
const userShares =
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
lock.initialTrancheShares,
|
|
495
|
-
),
|
|
496
|
-
sharesBeforeEpoch: parseFloat(
|
|
497
|
-
sharesAsOf(lockUpdates, epoch - 1)
|
|
498
|
-
?.shares ?? lock.initialTrancheShares,
|
|
499
|
-
),
|
|
500
|
-
};
|
|
501
|
-
},
|
|
502
|
-
);
|
|
503
|
-
|
|
504
|
-
const { total } = epochTrancheYield({
|
|
505
|
-
epochInterest: parseFloat(record.epochInterestAmount),
|
|
506
|
-
userShares,
|
|
507
|
-
trancheShares: parseFloat(
|
|
508
|
-
sharesAsOf(trancheUpdates, epoch)?.shares ?? '0',
|
|
509
|
-
),
|
|
510
|
-
trancheBalanceNow,
|
|
511
|
-
trancheSharesNow,
|
|
512
|
-
locks,
|
|
513
|
-
});
|
|
842
|
+
const clearingStart = poolStarts?.get(epoch);
|
|
843
|
+
|
|
844
|
+
const { yield: epochYield, userShares } = positionEpochYield(
|
|
845
|
+
{
|
|
846
|
+
trancheBalanceNow,
|
|
847
|
+
trancheSharesNow,
|
|
848
|
+
userUpdates,
|
|
849
|
+
trancheUpdates,
|
|
850
|
+
exits: trancheWithdrawals,
|
|
851
|
+
locks: position.userLendingPoolTrancheFixedTermDepositLocks.map(
|
|
852
|
+
(lock) => ({
|
|
853
|
+
...lock,
|
|
854
|
+
updates: lockSharesByLock.get(lock.id) ?? [],
|
|
855
|
+
}),
|
|
856
|
+
),
|
|
857
|
+
},
|
|
858
|
+
epoch,
|
|
859
|
+
parseFloat(record.epochInterestAmount),
|
|
860
|
+
clearingStart,
|
|
861
|
+
);
|
|
862
|
+
const total = epochYield.total;
|
|
514
863
|
|
|
515
864
|
// Nothing held and nothing topped up: not part of this
|
|
516
865
|
// lender's history (a new lender's history is empty).
|