@kasufinance/kasu-sdk 2.13.0 → 2.14.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 +40 -1
- package/dist/bundle.cjs.js +580 -60
- package/dist/bundle.esm.js +577 -61
- package/dist/domain/index.d.ts +2 -2
- package/dist/domain/pool-tvl.d.ts +104 -0
- package/dist/domain/requests.d.ts +11 -10
- package/dist/facade/index.d.ts +2 -0
- package/dist/facade/strategies.d.ts +17 -1
- package/dist/facade/strategy-deployments.d.ts +65 -0
- package/dist/index.d.ts +1 -1
- package/dist/services/DataService/data-service.d.ts +29 -1
- package/dist/services/DataService/tvl-history.d.ts +49 -0
- package/dist/services/UserLending/subgraph-types.d.ts +2 -8
- package/dist/services/UserLending/types.d.ts +10 -0
- package/dist/services/UserLending/user-lending.d.ts +14 -0
- package/package.json +1 -1
- package/src/domain/index.ts +7 -2
- package/src/domain/pool-tvl.ts +281 -0
- package/src/domain/requests.ts +37 -23
- package/src/facade/index.ts +10 -0
- package/src/facade/strategies.ts +26 -1
- package/src/facade/strategy-deployments.ts +161 -0
- 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/services/UserLending/queries.ts +2 -7
- package/src/services/UserLending/subgraph-types.ts +2 -8
- package/src/services/UserLending/types.ts +10 -0
- package/src/services/UserLending/user-lending.ts +98 -40
|
@@ -53,13 +53,18 @@ import {
|
|
|
53
53
|
ClearingsScope,
|
|
54
54
|
createOffChainTvlReader,
|
|
55
55
|
distinctBlocks,
|
|
56
|
+
fetchClearingBefore,
|
|
57
|
+
fetchClearingBeforeBlock,
|
|
56
58
|
fetchOffChainTvlAtBlocks,
|
|
57
59
|
fetchPoolBalancesAtBlocks,
|
|
58
60
|
fetchPoolClearings,
|
|
59
61
|
GraphRequest,
|
|
62
|
+
needsClearingBeforeBlock,
|
|
60
63
|
OffChainTvlReader,
|
|
61
64
|
PastBlockCache,
|
|
65
|
+
pickOpeningClearing,
|
|
62
66
|
PoolTvlHistoryOptions,
|
|
67
|
+
PoolTvlHistoryResult,
|
|
63
68
|
resolvePoolTvlHistoryWindow,
|
|
64
69
|
runCancellable,
|
|
65
70
|
TVL_HISTORY_CACHE_DEPTH,
|
|
@@ -294,6 +299,47 @@ export class DataService {
|
|
|
294
299
|
poolId: string,
|
|
295
300
|
options: PoolTvlHistoryOptions = {},
|
|
296
301
|
): Promise<PoolTvlHistoryEntry[]> {
|
|
302
|
+
return (await this._poolTvlHistory(poolId, options, false)).entries;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* `getPoolTvlHistory` — the same options, errors and `entries` — plus
|
|
307
|
+
* `opening`: the pool's value just before the window, so a strategy
|
|
308
|
+
* summed across deployments (`combineTvlHistories`) knows what each leg
|
|
309
|
+
* held before its first point instead of counting it 0.
|
|
310
|
+
*
|
|
311
|
+
* `opening` is the newest finished clearing STRICTLY before the window's
|
|
312
|
+
* first block (by `pickOpeningClearing`), read at its own block exactly
|
|
313
|
+
* like every other point and through the same past-block cache; `null`
|
|
314
|
+
* when the pool has no clearing before the window. Extra cost over
|
|
315
|
+
* `getPoolTvlHistory`:
|
|
316
|
+
*
|
|
317
|
+
* - `count` only: one more clearing row in the same request (`count + 1`,
|
|
318
|
+
* which always reaches past the window when older history exists);
|
|
319
|
+
* - `fromEpoch` above 0: one more one-row subgraph request, in parallel
|
|
320
|
+
* with the clearings;
|
|
321
|
+
* - only when every left-out clearing finished in the window's first
|
|
322
|
+
* block (a catch-up block that closed several epochs at once): one
|
|
323
|
+
* more one-row request by block (`fetchClearingBeforeBlock`), after
|
|
324
|
+
* the clearings — the common case never makes it;
|
|
325
|
+
* - either way, the opening's block in the same batched time-travel read
|
|
326
|
+
* and one more `externalTVLOfPool` read (cached once final).
|
|
327
|
+
*
|
|
328
|
+
* `{ entries: [], opening: null }` wherever `getPoolTvlHistory` returns
|
|
329
|
+
* `[]`.
|
|
330
|
+
*/
|
|
331
|
+
async getPoolTvlHistoryWindow(
|
|
332
|
+
poolId: string,
|
|
333
|
+
options: PoolTvlHistoryOptions = {},
|
|
334
|
+
): Promise<PoolTvlHistoryResult> {
|
|
335
|
+
return await this._poolTvlHistory(poolId, options, true);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
private async _poolTvlHistory(
|
|
339
|
+
poolId: string,
|
|
340
|
+
options: PoolTvlHistoryOptions,
|
|
341
|
+
withOpening: boolean,
|
|
342
|
+
): Promise<PoolTvlHistoryResult> {
|
|
297
343
|
validatePoolTvlHistoryOptions(options);
|
|
298
344
|
if (!isAddress(poolId)) {
|
|
299
345
|
throw new TypeError(
|
|
@@ -324,6 +370,7 @@ export class DataService {
|
|
|
324
370
|
: null,
|
|
325
371
|
externalTvl ?? '',
|
|
326
372
|
cancellation,
|
|
373
|
+
withOpening,
|
|
327
374
|
),
|
|
328
375
|
);
|
|
329
376
|
}
|
|
@@ -334,7 +381,9 @@ export class DataService {
|
|
|
334
381
|
reader: OffChainTvlReader | null,
|
|
335
382
|
externalTvl: string,
|
|
336
383
|
cancellation: Cancellation,
|
|
337
|
-
|
|
384
|
+
withOpening: boolean,
|
|
385
|
+
): Promise<PoolTvlHistoryResult> {
|
|
386
|
+
const nothing: PoolTvlHistoryResult = { entries: [], opening: null };
|
|
338
387
|
const graph: GraphRequest = <T>(
|
|
339
388
|
document: string,
|
|
340
389
|
variables: Record<string, unknown>,
|
|
@@ -343,20 +392,32 @@ export class DataService {
|
|
|
343
392
|
this._graph.request<T>({ document, variables, signal });
|
|
344
393
|
const scope: ClearingsScope =
|
|
345
394
|
options.fromEpoch === undefined && options.count !== undefined
|
|
346
|
-
? {
|
|
395
|
+
? {
|
|
396
|
+
kind: 'newest',
|
|
397
|
+
// One past the window: the opening, when there is one.
|
|
398
|
+
count: options.count + (withOpening ? 1 : 0),
|
|
399
|
+
}
|
|
347
400
|
: { kind: 'from', fromEpoch: options.fromEpoch ?? 0 };
|
|
348
401
|
|
|
349
402
|
try {
|
|
350
|
-
const [currentEpochNumber, { clearings, snapshot }] =
|
|
403
|
+
const [currentEpochNumber, { clearings, snapshot }, before] =
|
|
351
404
|
await Promise.all([
|
|
352
405
|
this._systemVariablesAbi.currentEpochNumber(),
|
|
353
406
|
fetchPoolClearings(graph, pool, scope, cancellation),
|
|
407
|
+
withOpening && scope.kind === 'from' && scope.fromEpoch > 0
|
|
408
|
+
? fetchClearingBefore(
|
|
409
|
+
graph,
|
|
410
|
+
pool,
|
|
411
|
+
scope.fromEpoch,
|
|
412
|
+
cancellation,
|
|
413
|
+
)
|
|
414
|
+
: Promise.resolve(null),
|
|
354
415
|
]);
|
|
355
416
|
const currentEpoch = currentEpochNumber.toNumber();
|
|
356
417
|
|
|
357
418
|
const window = resolvePoolTvlHistoryWindow(options, currentEpoch);
|
|
358
419
|
const poolNow = snapshot.pool;
|
|
359
|
-
if (!window || !poolNow) return
|
|
420
|
+
if (!window || !poolNow) return nothing;
|
|
360
421
|
const head = snapshot.meta?.block;
|
|
361
422
|
if (!head || typeof head.timestamp !== 'number') {
|
|
362
423
|
throw new Error(
|
|
@@ -369,7 +430,30 @@ export class DataService {
|
|
|
369
430
|
c.epochId >= window.fromEpoch &&
|
|
370
431
|
c.epochId <= window.toEpoch,
|
|
371
432
|
);
|
|
372
|
-
|
|
433
|
+
// Clearings the window left out, older by epoch: the extra row of
|
|
434
|
+
// a count fetch, or the one-row read below `fromEpoch`.
|
|
435
|
+
const older =
|
|
436
|
+
scope.kind === 'newest'
|
|
437
|
+
? clearings.filter((c) => c.epochId < window.fromEpoch)
|
|
438
|
+
: before === null
|
|
439
|
+
? []
|
|
440
|
+
: [before];
|
|
441
|
+
let opening = withOpening
|
|
442
|
+
? pickOpeningClearing(older, inWindow)
|
|
443
|
+
: null;
|
|
444
|
+
// The left-out clearings all sat in the first in-window block (a
|
|
445
|
+
// catch-up block): ask for the newest one before it, by block.
|
|
446
|
+
if (withOpening && needsClearingBeforeBlock(older, opening)) {
|
|
447
|
+
opening = await fetchClearingBeforeBlock(
|
|
448
|
+
graph,
|
|
449
|
+
pool,
|
|
450
|
+
Math.min(...inWindow.map((c) => c.block)),
|
|
451
|
+
cancellation,
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
const blocks = distinctBlocks(
|
|
455
|
+
opening ? [...inWindow, opening] : inWindow,
|
|
456
|
+
);
|
|
373
457
|
const withCurrent = window.toEpoch >= currentEpoch;
|
|
374
458
|
const cacheable = (block: number): boolean =>
|
|
375
459
|
block <= head.number - TVL_HISTORY_CACHE_DEPTH;
|
|
@@ -393,7 +477,7 @@ export class DataService {
|
|
|
393
477
|
},
|
|
394
478
|
]);
|
|
395
479
|
|
|
396
|
-
|
|
480
|
+
const entries = buildPoolTvlHistory({
|
|
397
481
|
clearings: inWindow,
|
|
398
482
|
balances,
|
|
399
483
|
offChain: offChainRead.offChain,
|
|
@@ -407,9 +491,21 @@ export class DataService {
|
|
|
407
491
|
: null,
|
|
408
492
|
decimals: this._kasuConfig.stableAssetDecimals,
|
|
409
493
|
});
|
|
494
|
+
// The same function as every other point, so the opening cannot
|
|
495
|
+
// be computed another way.
|
|
496
|
+
const [openingEntry] = opening
|
|
497
|
+
? buildPoolTvlHistory({
|
|
498
|
+
clearings: [opening],
|
|
499
|
+
balances,
|
|
500
|
+
offChain: offChainRead.offChain,
|
|
501
|
+
current: null,
|
|
502
|
+
decimals: this._kasuConfig.stableAssetDecimals,
|
|
503
|
+
})
|
|
504
|
+
: [null];
|
|
505
|
+
return { entries, opening: openingEntry };
|
|
410
506
|
} catch (error) {
|
|
411
507
|
if (isEmptyHistoryError(error, this._kasuConfig.isLiteDeployment)) {
|
|
412
|
-
return
|
|
508
|
+
return nothing;
|
|
413
509
|
}
|
|
414
510
|
throw error;
|
|
415
511
|
}
|
|
@@ -37,6 +37,22 @@ export interface PoolTvlHistoryOptions {
|
|
|
37
37
|
signal?: AbortSignal;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* `getPoolTvlHistoryWindow`'s answer: the same entries `getPoolTvlHistory`
|
|
42
|
+
* returns, and the pool's value just before them. Pass it to
|
|
43
|
+
* `combineTvlHistories` as a leg (`{ ...result, usdRate }`).
|
|
44
|
+
*/
|
|
45
|
+
export interface PoolTvlHistoryResult {
|
|
46
|
+
entries: PoolTvlHistoryEntry[];
|
|
47
|
+
/**
|
|
48
|
+
* The newest finished clearing BEFORE the window, in a block strictly
|
|
49
|
+
* before the first entry's, read at its own block like every other point
|
|
50
|
+
* (`kind: 'clearing'`). `null` when the pool has no clearing before the
|
|
51
|
+
* window — its whole history is in `entries`. See `pickOpeningClearing`.
|
|
52
|
+
*/
|
|
53
|
+
opening: PoolTvlHistoryEntry | null;
|
|
54
|
+
}
|
|
55
|
+
|
|
40
56
|
/** The resolved, inclusive epoch range; `null` when it is empty. */
|
|
41
57
|
export interface PoolTvlHistoryWindow {
|
|
42
58
|
fromEpoch: number;
|
|
@@ -449,6 +465,121 @@ export async function fetchPoolClearings(
|
|
|
449
465
|
return { clearings, snapshot, requests };
|
|
450
466
|
}
|
|
451
467
|
|
|
468
|
+
/**
|
|
469
|
+
* Pure: the opening clearing — the newest of `before` (finished clearings the
|
|
470
|
+
* window left out, older by epoch) that finished STRICTLY before the first
|
|
471
|
+
* in-window clearing's block. Newest by block, then epoch, so a clearing
|
|
472
|
+
* numbered out of order (XDC AUDD's epoch 1, a week before epoch 88) is
|
|
473
|
+
* placed by when it happened. A clearing in the first in-window block itself
|
|
474
|
+
* (a catch-up block that finished several epochs at once) is not an opening:
|
|
475
|
+
* it holds the first entry's own value, not the one before it. `null` when
|
|
476
|
+
* there is none — see `needsClearingBeforeBlock`.
|
|
477
|
+
*/
|
|
478
|
+
export function pickOpeningClearing(
|
|
479
|
+
before: readonly Clearing[],
|
|
480
|
+
inWindow: readonly Clearing[],
|
|
481
|
+
): Clearing | null {
|
|
482
|
+
const firstBlock = Math.min(...inWindow.map((c) => c.block));
|
|
483
|
+
let opening: Clearing | null = null;
|
|
484
|
+
for (const clearing of before) {
|
|
485
|
+
if (clearing.block >= firstBlock) continue;
|
|
486
|
+
if (
|
|
487
|
+
opening === null ||
|
|
488
|
+
clearing.block > opening.block ||
|
|
489
|
+
(clearing.block === opening.block &&
|
|
490
|
+
clearing.epochId > opening.epochId)
|
|
491
|
+
) {
|
|
492
|
+
opening = clearing;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
return opening;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Pure: whether the opening needs `fetchClearingBefore`'s by-block read.
|
|
500
|
+
* Only when the window left clearings out but every one of them finished in
|
|
501
|
+
* or after the first in-window block — the extra rows sat in a catch-up
|
|
502
|
+
* block — so an older one, if any, was not fetched. Otherwise the opening
|
|
503
|
+
* (or its absence) is already known and nothing more is read.
|
|
504
|
+
*/
|
|
505
|
+
export function needsClearingBeforeBlock(
|
|
506
|
+
before: readonly Clearing[],
|
|
507
|
+
opening: Clearing | null,
|
|
508
|
+
): boolean {
|
|
509
|
+
return opening === null && before.length > 0;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* The newest finished clearing in a block below `blockLt`, by block: the
|
|
514
|
+
* opening when every clearing the window left out shares its first block.
|
|
515
|
+
* ONE extra request for one row, no snapshot. Clearings that share that
|
|
516
|
+
* older block share its time and balance, so which one comes back does not
|
|
517
|
+
* change the value.
|
|
518
|
+
*/
|
|
519
|
+
export const poolTvlClearingBeforeBlockQuery = gql`
|
|
520
|
+
query PoolTvlClearingBeforeBlock($pool: String!, $blockLt: BigInt!) {
|
|
521
|
+
clearings: lendingPoolClearings(
|
|
522
|
+
first: 1
|
|
523
|
+
orderBy: endBlockNumber
|
|
524
|
+
orderDirection: desc
|
|
525
|
+
where: {
|
|
526
|
+
lendingPool: $pool
|
|
527
|
+
isFinished: true
|
|
528
|
+
endBlockNumber_lt: $blockLt
|
|
529
|
+
}
|
|
530
|
+
) {
|
|
531
|
+
epochId
|
|
532
|
+
endBlockNumber
|
|
533
|
+
endTimestamp
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
`;
|
|
537
|
+
|
|
538
|
+
export async function fetchClearingBeforeBlock(
|
|
539
|
+
request: GraphRequest,
|
|
540
|
+
poolId: string,
|
|
541
|
+
blockLt: number,
|
|
542
|
+
cancellation: Cancellation,
|
|
543
|
+
): Promise<Clearing | null> {
|
|
544
|
+
cancellation.throwIfCancelled();
|
|
545
|
+
const page = await request<{ clearings: ClearingRow[] }>(
|
|
546
|
+
poolTvlClearingBeforeBlockQuery,
|
|
547
|
+
{ pool: poolId, blockLt: String(blockLt) },
|
|
548
|
+
cancellation.signal,
|
|
549
|
+
);
|
|
550
|
+
const row = page.clearings.at(0);
|
|
551
|
+
return row === undefined ? null : parseClearing(row);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* The newest finished clearing numbered below `beforeEpoch`: the opening
|
|
556
|
+
* candidate of a `fromEpoch` window, whose own fetch never reads that far
|
|
557
|
+
* back. ONE extra request for one row, no snapshot.
|
|
558
|
+
*/
|
|
559
|
+
export async function fetchClearingBefore(
|
|
560
|
+
request: GraphRequest,
|
|
561
|
+
poolId: string,
|
|
562
|
+
beforeEpoch: number,
|
|
563
|
+
cancellation: Cancellation,
|
|
564
|
+
): Promise<Clearing | null> {
|
|
565
|
+
cancellation.throwIfCancelled();
|
|
566
|
+
const page = await request<PoolTvlClearingsResult>(
|
|
567
|
+
poolTvlClearingsQuery,
|
|
568
|
+
{
|
|
569
|
+
poolId,
|
|
570
|
+
pool: poolId,
|
|
571
|
+
epochGte: '0',
|
|
572
|
+
epochLt: String(beforeEpoch),
|
|
573
|
+
direction: 'desc',
|
|
574
|
+
first: 1,
|
|
575
|
+
withSnapshot: false,
|
|
576
|
+
},
|
|
577
|
+
cancellation.signal,
|
|
578
|
+
);
|
|
579
|
+
const row = page.clearings.at(0);
|
|
580
|
+
return row === undefined ? null : parseClearing(row);
|
|
581
|
+
}
|
|
582
|
+
|
|
452
583
|
/**
|
|
453
584
|
* The pool's balance (display units, the subgraph's decimal string) at the end
|
|
454
585
|
* of each block, `TVL_HISTORY_BLOCKS_PER_REQUEST` blocks per request. A block
|
|
@@ -160,14 +160,13 @@ export const currentEpochFtdAmountQuery = gql`
|
|
|
160
160
|
user: $userId
|
|
161
161
|
lendingPool_in: $poolIds
|
|
162
162
|
epochId: $currentEpoch
|
|
163
|
+
type: DepositRequest
|
|
163
164
|
}
|
|
164
165
|
) {
|
|
165
166
|
fixedTermConfigId
|
|
166
167
|
amountRequested
|
|
167
168
|
tranche {
|
|
168
169
|
id
|
|
169
|
-
balance
|
|
170
|
-
shares
|
|
171
170
|
}
|
|
172
171
|
}
|
|
173
172
|
}
|
|
@@ -183,11 +182,7 @@ export const currentFtdBalanceQuery = gql`
|
|
|
183
182
|
configId
|
|
184
183
|
lendingPoolTranche {
|
|
185
184
|
id
|
|
186
|
-
|
|
187
|
-
}
|
|
188
|
-
lendingPool {
|
|
189
|
-
balance
|
|
190
|
-
tranches {
|
|
185
|
+
balance
|
|
191
186
|
shares
|
|
192
187
|
}
|
|
193
188
|
}
|
|
@@ -77,8 +77,6 @@ export interface CurrentEpochFtdAmountSubgraph {
|
|
|
77
77
|
amountRequested: string;
|
|
78
78
|
tranche: {
|
|
79
79
|
id: string;
|
|
80
|
-
balance: string;
|
|
81
|
-
shares: string;
|
|
82
80
|
};
|
|
83
81
|
}[];
|
|
84
82
|
}
|
|
@@ -161,13 +159,9 @@ export interface CurrentFtdBalanceSubgraph {
|
|
|
161
159
|
configId: string;
|
|
162
160
|
lendingPoolTranche: {
|
|
163
161
|
id: string;
|
|
164
|
-
|
|
165
|
-
};
|
|
166
|
-
lendingPool: {
|
|
167
|
-
balance: string;
|
|
168
|
-
tranches: {
|
|
162
|
+
balance: string;
|
|
169
163
|
shares: string;
|
|
170
|
-
}
|
|
164
|
+
};
|
|
171
165
|
};
|
|
172
166
|
}[];
|
|
173
167
|
}
|
|
@@ -39,8 +39,18 @@ export interface UserRequest {
|
|
|
39
39
|
requestType: 'Deposit' | 'Withdrawal';
|
|
40
40
|
trancheId: string;
|
|
41
41
|
trancheName: string;
|
|
42
|
+
/**
|
|
43
|
+
* The amount still OUTSTANDING on the request, in assets (the
|
|
44
|
+
* deployment's stable token, display units) — not the amount first asked
|
|
45
|
+
* for: the subgraph takes accepted and rejected amounts off it, and a
|
|
46
|
+
* cancellation zeroes it. A deposit's figure is the asset amount as the
|
|
47
|
+
* subgraph holds it (top-ups included); a withdrawal's is its remaining
|
|
48
|
+
* tranche shares at the tranche's current share price.
|
|
49
|
+
*/
|
|
42
50
|
requestedAmount: string;
|
|
51
|
+
/** Assets accepted so far — deposited, or paid out for a withdrawal. */
|
|
43
52
|
acceptedAmount: string;
|
|
53
|
+
/** Assets rejected and returned — deposits only; 0 for a withdrawal. */
|
|
44
54
|
rejectedAmount: string;
|
|
45
55
|
timestamp: EpochTimeStamp;
|
|
46
56
|
status: UserRequestStatus;
|
|
@@ -8,13 +8,7 @@ import {
|
|
|
8
8
|
ethers,
|
|
9
9
|
Signer,
|
|
10
10
|
} from 'ethers';
|
|
11
|
-
import {
|
|
12
|
-
defaultAbiCoder,
|
|
13
|
-
formatEther,
|
|
14
|
-
formatUnits,
|
|
15
|
-
parseEther,
|
|
16
|
-
parseUnits,
|
|
17
|
-
} from 'ethers/lib/utils';
|
|
11
|
+
import { defaultAbiCoder, formatUnits, parseUnits } from 'ethers/lib/utils';
|
|
18
12
|
import { GraphQLClient } from 'graphql-request';
|
|
19
13
|
|
|
20
14
|
import {
|
|
@@ -70,6 +64,20 @@ import {
|
|
|
70
64
|
UserTrancheBalance,
|
|
71
65
|
} from './types';
|
|
72
66
|
|
|
67
|
+
/** Thrown when the subgraph reports a request type with no known unit. */
|
|
68
|
+
export const UNKNOWN_REQUEST_TYPE_MESSAGE =
|
|
69
|
+
'UserLending: unknown request type, its amount unit is unknown';
|
|
70
|
+
|
|
71
|
+
const KNOWN_REQUEST_TYPES: readonly string[] = [
|
|
72
|
+
'DepositRequest',
|
|
73
|
+
'WithdrawalRequest',
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
const isKnownRequestType = (
|
|
77
|
+
type: unknown,
|
|
78
|
+
): type is UserRequestsSubgraph['userRequests'][number]['type'] =>
|
|
79
|
+
typeof type === 'string' && KNOWN_REQUEST_TYPES.includes(type);
|
|
80
|
+
|
|
73
81
|
export class UserLending {
|
|
74
82
|
private readonly _graph: GraphQLClient;
|
|
75
83
|
|
|
@@ -406,6 +414,17 @@ export class UserLending {
|
|
|
406
414
|
const retn: UserRequest[] = [];
|
|
407
415
|
|
|
408
416
|
for (const userRequest of subgraphResult.userRequests) {
|
|
417
|
+
// A request type this SDK does not know has no known amount unit
|
|
418
|
+
// (the schema also declares types no live deployment emits yet).
|
|
419
|
+
// Skip that ONE row before anything is priced or read on chain,
|
|
420
|
+
// rather than failing — and blanking — the lender's whole list.
|
|
421
|
+
if (!isKnownRequestType(userRequest.type)) {
|
|
422
|
+
console.error(
|
|
423
|
+
`${UNKNOWN_REQUEST_TYPE_MESSAGE}: ${String(userRequest.type)} (request ${userRequest.id}); row skipped`,
|
|
424
|
+
);
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
|
|
409
428
|
const directusPool = directusResult.find(
|
|
410
429
|
({ id }) => id === userRequest.lendingPool.id,
|
|
411
430
|
);
|
|
@@ -552,13 +571,7 @@ export class UserLending {
|
|
|
552
571
|
: 'Withdrawal',
|
|
553
572
|
trancheName: trancheName,
|
|
554
573
|
trancheId: userRequest.tranche.id,
|
|
555
|
-
requestedAmount:
|
|
556
|
-
? userRequest.amountRequested
|
|
557
|
-
: this.convertSharesToAssets(
|
|
558
|
-
userRequest.amountRequested,
|
|
559
|
-
userRequest.tranche.balance,
|
|
560
|
-
userRequest.tranche.shares,
|
|
561
|
-
).toString(),
|
|
574
|
+
requestedAmount: this.requestedAmountInAssets(userRequest),
|
|
562
575
|
acceptedAmount: userRequest.amountAccepted,
|
|
563
576
|
rejectedAmount: userRequest.amountRejected,
|
|
564
577
|
status: userRequest.status,
|
|
@@ -606,15 +619,16 @@ export class UserLending {
|
|
|
606
619
|
const trancheID =
|
|
607
620
|
userFTD.lendingPoolTrancheFixedTermConfig.lendingPoolTranche.id.toLowerCase();
|
|
608
621
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
622
|
+
// The lock holds shares of ONE tranche, so they are worth that
|
|
623
|
+
// tranche's share price — not the pool's average across tranches,
|
|
624
|
+
// which differs whenever the tranches have earned differently.
|
|
625
|
+
const lockTranche =
|
|
626
|
+
userFTD.lendingPoolTrancheFixedTermConfig.lendingPoolTranche;
|
|
613
627
|
|
|
614
628
|
const amount = this.convertSharesToAssets(
|
|
615
629
|
userFTD.trancheShares,
|
|
616
|
-
|
|
617
|
-
|
|
630
|
+
lockTranche.balance,
|
|
631
|
+
lockTranche.shares,
|
|
618
632
|
);
|
|
619
633
|
|
|
620
634
|
const item = trancheMap.get(trancheID) ?? [];
|
|
@@ -643,33 +657,43 @@ export class UserLending {
|
|
|
643
657
|
currentEpoch: parseFloat(currentEpoch),
|
|
644
658
|
});
|
|
645
659
|
|
|
646
|
-
|
|
660
|
+
// Deposit requests only (the query filters on type): a deposit's
|
|
661
|
+
// `amountRequested` is already an ASSET amount, so it is summed as it
|
|
662
|
+
// is — never put through a share price. Summed, not assigned: a
|
|
663
|
+
// cancelled request and its replacement share a tranche and a config
|
|
664
|
+
// in one epoch, and the cancelled one's 0 must not overwrite the live
|
|
665
|
+
// amount.
|
|
666
|
+
const totals = new Map<string, Map<number, BigNumber>>();
|
|
647
667
|
|
|
648
668
|
for (const userRequest of currentEpochFtdAmountRes.userRequests) {
|
|
649
669
|
const ftdID = parseFloat(userRequest.fixedTermConfigId);
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
670
|
+
if (!Number.isInteger(ftdID) || ftdID < 0) continue;
|
|
671
|
+
|
|
672
|
+
const perConfig =
|
|
673
|
+
totals.get(userRequest.tranche.id) ??
|
|
674
|
+
new Map<number, BigNumber>();
|
|
675
|
+
perConfig.set(
|
|
676
|
+
ftdID,
|
|
677
|
+
(perConfig.get(ftdID) ?? BigNumber.from(0)).add(
|
|
678
|
+
parseUnits(userRequest.amountRequested),
|
|
679
|
+
),
|
|
657
680
|
);
|
|
681
|
+
totals.set(userRequest.tranche.id, perConfig);
|
|
682
|
+
}
|
|
658
683
|
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
currentEpochFtdAmounts.set(userRequest.tranche.id, clonedItem);
|
|
684
|
+
// A slot whose total is zero is left out — `'0.0'` is a truthy
|
|
685
|
+
// string, and a caller testing the slot would read it as an amount.
|
|
686
|
+
// A tranche with no non-zero slot is left out entirely.
|
|
687
|
+
const currentEpochFtdAmounts = new Map<string, string[]>();
|
|
665
688
|
|
|
666
|
-
|
|
667
|
-
}
|
|
689
|
+
for (const [trancheId, perConfig] of totals) {
|
|
668
690
|
const ftdAmounts: string[] = [];
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
691
|
+
for (const [ftdID, total] of perConfig) {
|
|
692
|
+
if (total.gt(0)) ftdAmounts[ftdID] = formatUnits(total);
|
|
693
|
+
}
|
|
694
|
+
if (ftdAmounts.length) {
|
|
695
|
+
currentEpochFtdAmounts.set(trancheId, ftdAmounts);
|
|
696
|
+
}
|
|
673
697
|
}
|
|
674
698
|
|
|
675
699
|
return currentEpochFtdAmounts;
|
|
@@ -708,6 +732,40 @@ export class UserLending {
|
|
|
708
732
|
return amountMap;
|
|
709
733
|
}
|
|
710
734
|
|
|
735
|
+
/**
|
|
736
|
+
* A request's outstanding amount in ASSETS (the deployment's stable token).
|
|
737
|
+
*
|
|
738
|
+
* The subgraph's `UserRequest.amountRequested` is denominated by request
|
|
739
|
+
* type. A DEPOSIT request holds assets: the deposit NFT's `assetAmount`,
|
|
740
|
+
* top-ups added, accepted and rejected amounts taken off. A WITHDRAWAL
|
|
741
|
+
* request holds tranche SHARES: the withdrawal NFT's `sharesAmount`,
|
|
742
|
+
* accepted shares taken off. So only a withdrawal is converted, at the
|
|
743
|
+
* tranche's current share price; a deposit passes through unchanged —
|
|
744
|
+
* pricing it as shares overstated it by the share price.
|
|
745
|
+
*/
|
|
746
|
+
private requestedAmountInAssets(
|
|
747
|
+
userRequest: UserRequestsSubgraph['userRequests'][number],
|
|
748
|
+
): string {
|
|
749
|
+
switch (userRequest.type) {
|
|
750
|
+
case 'DepositRequest':
|
|
751
|
+
return userRequest.amountRequested;
|
|
752
|
+
case 'WithdrawalRequest':
|
|
753
|
+
return this.convertSharesToAssets(
|
|
754
|
+
userRequest.amountRequested,
|
|
755
|
+
userRequest.tranche.balance,
|
|
756
|
+
userRequest.tranche.shares,
|
|
757
|
+
);
|
|
758
|
+
default: {
|
|
759
|
+
// Fail closed: a type this SDK does not know has no known
|
|
760
|
+
// unit, and pricing it either way would print a wrong figure.
|
|
761
|
+
const unknownType: string = userRequest.type;
|
|
762
|
+
throw new Error(
|
|
763
|
+
`${UNKNOWN_REQUEST_TYPE_MESSAGE}: ${unknownType}`,
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
711
769
|
convertSharesToAssets(
|
|
712
770
|
sharesAmount: string,
|
|
713
771
|
totalAssets: string,
|