@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
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { Provider } from '@ethersproject/providers';
|
|
2
2
|
import { BigNumber, Signer } from 'ethers';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
formatEther,
|
|
5
|
+
formatUnits,
|
|
6
|
+
parseEther,
|
|
7
|
+
parseUnits,
|
|
8
|
+
} from 'ethers/lib/utils';
|
|
4
9
|
import { GraphQLClient } from 'graphql-request';
|
|
5
10
|
|
|
6
11
|
import {
|
|
@@ -11,7 +16,7 @@ import {
|
|
|
11
16
|
IUserLoyaltyRewardsAbi__factory,
|
|
12
17
|
} from '../../contracts';
|
|
13
18
|
import {
|
|
14
|
-
|
|
19
|
+
EpochTrancheYield,
|
|
15
20
|
YieldHistoryEntry,
|
|
16
21
|
} from '../../domain/yield-history';
|
|
17
22
|
import { SdkConfig } from '../../sdk-config';
|
|
@@ -35,10 +40,12 @@ import {
|
|
|
35
40
|
LockingSummarySubgraphResult,
|
|
36
41
|
SystemVariables,
|
|
37
42
|
} from '../Locking/types';
|
|
43
|
+
import { isBlockUnavailableError } from '../shared';
|
|
38
44
|
import { UserLending } from '../UserLending/user-lending';
|
|
39
45
|
|
|
40
46
|
import { lendingPortfolioQuery } from './queries';
|
|
41
47
|
import {
|
|
48
|
+
LedgerMismatch,
|
|
42
49
|
LendingPortfolioQueryResult,
|
|
43
50
|
PortfolioLendingPool,
|
|
44
51
|
PortfolioRewards,
|
|
@@ -49,8 +56,11 @@ import {
|
|
|
49
56
|
} from './types';
|
|
50
57
|
import {
|
|
51
58
|
buildYieldHistory,
|
|
59
|
+
clearingStarts,
|
|
52
60
|
fetchYieldHistorySeries,
|
|
53
61
|
isEmptyHistoryError,
|
|
62
|
+
positionEpochYield,
|
|
63
|
+
positionSeriesFrom,
|
|
54
64
|
resolveYieldHistoryWindow,
|
|
55
65
|
validateYieldHistoryOptions,
|
|
56
66
|
YIELD_HISTORY_PAGE_SIZE,
|
|
@@ -61,6 +71,74 @@ import {
|
|
|
61
71
|
YieldHistoryPositionsResult,
|
|
62
72
|
} from './yield-history';
|
|
63
73
|
|
|
74
|
+
/**
|
|
75
|
+
* How many epochs back the portfolio read pairs a tranche's newest interest
|
|
76
|
+
* with the shares in force at its clearing (`lendingPortfolioQuery`): the last
|
|
77
|
+
* six settled epochs, so a pool whose clearings run late or lag a few weeks
|
|
78
|
+
* behind the others (Base has one) is paired too. A pool whose newest
|
|
79
|
+
* interest is older (pools clear independently; some only when the
|
|
80
|
+
* originator repays) is priced from a history read of its own, at that
|
|
81
|
+
* epoch. The extra rows are a few per lender and tranche.
|
|
82
|
+
*/
|
|
83
|
+
const RECENT_PAIRING_EPOCHS = 6;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The ledger (subgraph) and the chain agree on a balance AT THE SAME BLOCK
|
|
87
|
+
* within a cent, or a millionth of it — the rounding of two share-price
|
|
88
|
+
* conversions.
|
|
89
|
+
*/
|
|
90
|
+
const BALANCE_RECONCILE_TOLERANCE = 0.01;
|
|
91
|
+
const BALANCE_RECONCILE_RELATIVE = 1e-6;
|
|
92
|
+
|
|
93
|
+
function ledgerAgrees(ledger: number, chain: number): boolean {
|
|
94
|
+
return (
|
|
95
|
+
Math.abs(ledger - chain) <=
|
|
96
|
+
Math.max(
|
|
97
|
+
BALANCE_RECONCILE_TOLERANCE,
|
|
98
|
+
Math.abs(chain) * BALANCE_RECONCILE_RELATIVE,
|
|
99
|
+
)
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* A list read with `first: $first` that comes back FULL may have been cut:
|
|
105
|
+
* refuse it rather than price from a truncated read.
|
|
106
|
+
*/
|
|
107
|
+
function refuseTruncated(data: LendingPortfolioQueryResult): void {
|
|
108
|
+
const full = (rows: readonly unknown[] | undefined): boolean =>
|
|
109
|
+
(rows?.length ?? 0) >= YIELD_HISTORY_PAGE_SIZE;
|
|
110
|
+
const lists: [string, boolean][] = [
|
|
111
|
+
['lendingPoolClearings', full(data.lendingPoolClearings)],
|
|
112
|
+
['recentWithdrawalsAccepted', full(data.recentWithdrawalsAccepted)],
|
|
113
|
+
];
|
|
114
|
+
for (const pool of data.user?.lendingPoolUserDetails ?? []) {
|
|
115
|
+
for (const position of pool.lendingPoolTrancheUserDetails) {
|
|
116
|
+
lists.push(
|
|
117
|
+
[
|
|
118
|
+
'recentUserShareUpdates',
|
|
119
|
+
full(position.recentUserShareUpdates),
|
|
120
|
+
],
|
|
121
|
+
[
|
|
122
|
+
'recentTrancheShareUpdates',
|
|
123
|
+
full(position.tranche.recentTrancheShareUpdates),
|
|
124
|
+
],
|
|
125
|
+
...position.userLendingPoolTrancheFixedTermDepositLocks.map(
|
|
126
|
+
(lock): [string, boolean] => [
|
|
127
|
+
'lockSharesRecent',
|
|
128
|
+
full(lock.lockSharesRecent),
|
|
129
|
+
],
|
|
130
|
+
),
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const cut = lists.find(([, isFull]) => isFull);
|
|
135
|
+
if (cut) {
|
|
136
|
+
throw new Error(
|
|
137
|
+
`getPortfolioLendingData: ${cut[0]} reached ${String(YIELD_HISTORY_PAGE_SIZE)} rows; refusing a possibly truncated read`,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
64
142
|
export class Portfolio {
|
|
65
143
|
private _lockingService: KSULocking;
|
|
66
144
|
private _dataService: DataService;
|
|
@@ -451,30 +529,94 @@ export class Portfolio {
|
|
|
451
529
|
const previousEpoch = BigNumber.from(currentEpoch).sub(
|
|
452
530
|
BigNumber.from(1),
|
|
453
531
|
);
|
|
532
|
+
const recentEpoch = Math.max(
|
|
533
|
+
0,
|
|
534
|
+
previousEpoch.toNumber() - (RECENT_PAIRING_EPOCHS - 1),
|
|
535
|
+
);
|
|
454
536
|
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
537
|
+
const unused = new Set(
|
|
538
|
+
this._kasuConfig.UNUSED_LENDING_POOL_IDS.map((id) =>
|
|
539
|
+
id.toLowerCase(),
|
|
540
|
+
),
|
|
541
|
+
);
|
|
542
|
+
const listedPools = poolOverviews.filter(
|
|
543
|
+
({ id }) => !unused.has(id.toLowerCase()),
|
|
544
|
+
);
|
|
545
|
+
|
|
546
|
+
const lastEpochData =
|
|
547
|
+
await this._graph.request<LendingPortfolioQueryResult>(
|
|
548
|
+
lendingPortfolioQuery,
|
|
549
|
+
{
|
|
550
|
+
userAddress: userAddress.toLowerCase(),
|
|
551
|
+
epochId: parseFloat(currentEpoch),
|
|
552
|
+
lastEpochId: previousEpoch.toNumber(),
|
|
553
|
+
recentEpoch,
|
|
554
|
+
lockRecentEpoch: Math.max(0, recentEpoch - 1),
|
|
555
|
+
unusedPools: this._kasuConfig.UNUSED_LENDING_POOL_IDS,
|
|
556
|
+
poolIds: listedPools.map(({ id }) => id.toLowerCase()),
|
|
557
|
+
trancheIds: listedPools.flatMap(({ tranches }) =>
|
|
558
|
+
tranches.map(({ id }) => id.toLowerCase()),
|
|
559
|
+
),
|
|
560
|
+
first: YIELD_HISTORY_PAGE_SIZE,
|
|
561
|
+
},
|
|
562
|
+
);
|
|
563
|
+
|
|
564
|
+
if (!lastEpochData.user) return [];
|
|
565
|
+
refuseTruncated(lastEpochData);
|
|
566
|
+
|
|
567
|
+
// The pools the lender has a ledger in AND that are listed: the only
|
|
568
|
+
// ones read on chain, and the only ones reported.
|
|
569
|
+
const userPools = listedPools.flatMap((overview) => {
|
|
570
|
+
const details = lastEpochData.user?.lendingPoolUserDetails.find(
|
|
571
|
+
({ lendingPool }) =>
|
|
572
|
+
lendingPool.id.toLowerCase() === overview.id.toLowerCase(),
|
|
573
|
+
);
|
|
574
|
+
return details ? [{ overview, details }] : [];
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
const recentStarts = clearingStarts(lastEpochData.lendingPoolClearings);
|
|
578
|
+
const recentExits = new Map<
|
|
579
|
+
string,
|
|
580
|
+
LendingPortfolioQueryResult['recentWithdrawalsAccepted']
|
|
581
|
+
>();
|
|
582
|
+
for (const exit of lastEpochData.recentWithdrawalsAccepted) {
|
|
583
|
+
const list = recentExits.get(exit.tranche.id) ?? [];
|
|
584
|
+
list.push(exit);
|
|
585
|
+
recentExits.set(exit.tranche.id, list);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const [chain, staleYields] = await Promise.all([
|
|
589
|
+
this.readChainAt(lastEpochData._meta?.block.number, (blockTag) =>
|
|
590
|
+
this._userLendingService.getUserPoolBalancesAt(
|
|
468
591
|
userAddress,
|
|
592
|
+
userPools.map(({ overview }) => overview.id),
|
|
593
|
+
provider,
|
|
594
|
+
blockTag,
|
|
595
|
+
),
|
|
596
|
+
),
|
|
597
|
+
this.staleLastEpochYields(
|
|
598
|
+
userPools.flatMap(({ overview, details }) =>
|
|
599
|
+
details.lendingPoolTrancheUserDetails.map((position) => ({
|
|
600
|
+
poolId: overview.id,
|
|
601
|
+
position,
|
|
602
|
+
})),
|
|
469
603
|
),
|
|
470
|
-
|
|
604
|
+
recentEpoch,
|
|
605
|
+
),
|
|
606
|
+
]);
|
|
607
|
+
|
|
608
|
+
const reconciled = await Promise.all(
|
|
609
|
+
userPools.map(({ overview, details }, index) =>
|
|
610
|
+
this.reconcilePool(
|
|
471
611
|
userAddress,
|
|
472
|
-
|
|
612
|
+
overview,
|
|
613
|
+
details,
|
|
614
|
+
chain.values[index] ?? BigNumber.from(0),
|
|
615
|
+
chain.blockTag,
|
|
473
616
|
provider,
|
|
474
617
|
),
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
if (!lastEpochData.user) return [];
|
|
618
|
+
),
|
|
619
|
+
);
|
|
478
620
|
|
|
479
621
|
const userRequestsMap = new Map<
|
|
480
622
|
string,
|
|
@@ -526,16 +668,12 @@ export class Portfolio {
|
|
|
526
668
|
}
|
|
527
669
|
}
|
|
528
670
|
|
|
529
|
-
|
|
671
|
+
for (const [
|
|
672
|
+
poolIndex,
|
|
673
|
+
{ overview: poolOverview, details },
|
|
674
|
+
] of userPools.entries()) {
|
|
675
|
+
const pool = reconciled[poolIndex];
|
|
530
676
|
|
|
531
|
-
for (const poolOverview of poolOverviews) {
|
|
532
|
-
const userPoolBalance = userPoolBalances.find(
|
|
533
|
-
({ poolId }) => poolId === poolOverview.id,
|
|
534
|
-
);
|
|
535
|
-
if (!userPoolBalance) {
|
|
536
|
-
continue;
|
|
537
|
-
}
|
|
538
|
-
totalInvestments = userPoolBalance.balance.add(totalInvestments);
|
|
539
677
|
const tranches: (Omit<PortfolioTranche, 'investedAmount'> & {
|
|
540
678
|
investedAmount: {
|
|
541
679
|
balance: string;
|
|
@@ -545,19 +683,13 @@ export class Portfolio {
|
|
|
545
683
|
|
|
546
684
|
let poolYieldEarnedLastEpoch = 0;
|
|
547
685
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
);
|
|
552
|
-
|
|
553
|
-
if (!lendingPoolUserDetails) continue;
|
|
554
|
-
|
|
555
|
-
const portfolioUserTrancheBalance =
|
|
556
|
-
portfolioUserTrancheBalances.get(poolOverview.id);
|
|
686
|
+
// A ledger crediting shares the wallet does not hold earns
|
|
687
|
+
// nothing we can vouch for: its last-cycle figures are withheld.
|
|
688
|
+
const withheld = pool.mismatch?.kind === 'ledger-exceeds-chain';
|
|
557
689
|
|
|
558
690
|
for (const tranche of poolOverview.tranches) {
|
|
559
691
|
const lastEpochUserTrancheDetails =
|
|
560
|
-
|
|
692
|
+
details.lendingPoolTrancheUserDetails.find(
|
|
561
693
|
(lendingPool) => lendingPool.tranche.id === tranche.id,
|
|
562
694
|
);
|
|
563
695
|
|
|
@@ -581,86 +713,83 @@ export class Portfolio {
|
|
|
581
713
|
});
|
|
582
714
|
continue;
|
|
583
715
|
}
|
|
584
|
-
const lastEpoch = previousEpoch.toNumber();
|
|
585
716
|
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
717
|
+
// The tranche's newest interest (epoch E), priced with the
|
|
718
|
+
// shares in force when clearing E applied it — the one
|
|
719
|
+
// selection `getYieldHistory` makes (`positionEpochYield`).
|
|
720
|
+
// Inside the recent window the candidates below are complete.
|
|
721
|
+
// An older E (a pool that clears rarely — some only when the
|
|
722
|
+
// originator repays) is priced from a history read of its own,
|
|
723
|
+
// and is 0 for a lender who has left the tranche.
|
|
724
|
+
const newestInterest =
|
|
725
|
+
lastEpochUserTrancheDetails.tranche
|
|
726
|
+
.lendingPoolTrancheEpochInterest?.[0];
|
|
727
|
+
const interestEpoch =
|
|
728
|
+
newestInterest?.epochId === undefined
|
|
729
|
+
? undefined
|
|
730
|
+
: Number(newestInterest.epochId);
|
|
590
731
|
|
|
591
732
|
const fixedTermDeposits =
|
|
592
733
|
lastEpochUserTrancheDetails.userLendingPoolTrancheFixedTermDepositLocks;
|
|
593
734
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
735
|
+
const noYield: EpochTrancheYield = {
|
|
736
|
+
total: 0,
|
|
737
|
+
variable: 0,
|
|
738
|
+
locks: fixedTermDeposits.map(() => 0),
|
|
739
|
+
};
|
|
740
|
+
let lastEpochYield = noYield;
|
|
741
|
+
if (
|
|
742
|
+
interestEpoch !== undefined &&
|
|
743
|
+
interestEpoch < recentEpoch &&
|
|
744
|
+
!withheld
|
|
745
|
+
) {
|
|
746
|
+
lastEpochYield =
|
|
747
|
+
staleYields.get(lastEpochUserTrancheDetails.id) ??
|
|
748
|
+
noYield;
|
|
749
|
+
} else if (interestEpoch !== undefined && !withheld) {
|
|
750
|
+
lastEpochYield = positionEpochYield(
|
|
751
|
+
{
|
|
752
|
+
trancheBalanceNow: parseFloat(
|
|
753
|
+
lastEpochUserTrancheDetails.tranche.balance,
|
|
754
|
+
),
|
|
755
|
+
trancheSharesNow: parseFloat(
|
|
756
|
+
lastEpochUserTrancheDetails.tranche.shares,
|
|
757
|
+
),
|
|
758
|
+
userUpdates: [
|
|
759
|
+
...(lastEpochUserTrancheDetails.lendingPoolTrancheUserEpochSharesUpdates ??
|
|
760
|
+
[]),
|
|
761
|
+
...(lastEpochUserTrancheDetails.recentUserShareUpdates ??
|
|
762
|
+
[]),
|
|
763
|
+
],
|
|
764
|
+
trancheUpdates: [
|
|
765
|
+
...(lastEpochUserTrancheDetails.tranche
|
|
766
|
+
.lendingPoolTrancheShareUpdates ?? []),
|
|
767
|
+
...(lastEpochUserTrancheDetails.tranche
|
|
768
|
+
.recentTrancheShareUpdates ?? []),
|
|
769
|
+
],
|
|
770
|
+
exits: recentExits.get(tranche.id) ?? [],
|
|
771
|
+
locks: fixedTermDeposits.map((lock) => ({
|
|
772
|
+
...lock,
|
|
773
|
+
updates: [
|
|
774
|
+
...(lock.lockSharesBase ?? []),
|
|
775
|
+
...(lock.lockSharesRecent ?? []),
|
|
776
|
+
],
|
|
777
|
+
})),
|
|
778
|
+
},
|
|
779
|
+
interestEpoch,
|
|
780
|
+
parseFloat(newestInterest?.epochInterestAmount ?? '0'),
|
|
781
|
+
recentStarts
|
|
782
|
+
.get(poolOverview.id.toLowerCase())
|
|
783
|
+
?.get(interestEpoch),
|
|
784
|
+
).yield;
|
|
785
|
+
}
|
|
644
786
|
|
|
787
|
+
const ledger = pool.tranches.get(tranche.id);
|
|
645
788
|
const userTrancheBalance = {
|
|
646
|
-
balance: '0',
|
|
647
|
-
yieldEarned: 0,
|
|
789
|
+
balance: ledger?.balance ?? '0',
|
|
790
|
+
yieldEarned: ledger?.yieldEarned ?? 0,
|
|
648
791
|
};
|
|
649
792
|
|
|
650
|
-
if (portfolioUserTrancheBalance) {
|
|
651
|
-
const portfolioUserTranche =
|
|
652
|
-
portfolioUserTrancheBalance.find(
|
|
653
|
-
({ trancheId }) => trancheId === tranche.id,
|
|
654
|
-
);
|
|
655
|
-
|
|
656
|
-
if (portfolioUserTranche) {
|
|
657
|
-
userTrancheBalance.balance =
|
|
658
|
-
portfolioUserTranche.userBalance;
|
|
659
|
-
userTrancheBalance.yieldEarned =
|
|
660
|
-
portfolioUserTranche.yieldEarned;
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
|
|
664
793
|
let totalLifetimeFtdYield = 0;
|
|
665
794
|
|
|
666
795
|
const fixedLoans: PortfolioTranche['fixedLoans'] =
|
|
@@ -754,9 +883,12 @@ export class Portfolio {
|
|
|
754
883
|
totalYieldEarningsLastEpoch: this.precisionToString(
|
|
755
884
|
poolYieldEarnedLastEpoch,
|
|
756
885
|
),
|
|
757
|
-
totalInvestedAmount:
|
|
758
|
-
totalYieldEarningsLifetime:
|
|
759
|
-
|
|
886
|
+
totalInvestedAmount: pool.chainBalance.toString(),
|
|
887
|
+
totalYieldEarningsLifetime: pool.lifetime.toString(),
|
|
888
|
+
...(pool.mismatch ? { ledgerMismatch: pool.mismatch } : {}),
|
|
889
|
+
...(chain.blockTag === undefined
|
|
890
|
+
? { chainPinned: false as const }
|
|
891
|
+
: {}),
|
|
760
892
|
tranches,
|
|
761
893
|
});
|
|
762
894
|
}
|
|
@@ -782,13 +914,22 @@ export class Portfolio {
|
|
|
782
914
|
tranche.yieldEarnings.lifetime,
|
|
783
915
|
).toFixed(6);
|
|
784
916
|
|
|
917
|
+
// Never negative: a variable balance below the locked
|
|
918
|
+
// amount is a ledger the chain does not back (the pool
|
|
919
|
+
// carries `ledgerMismatch`), not money owed — flagged.
|
|
920
|
+
const variableBalance = parseEther(
|
|
921
|
+
trancheBalance.balance,
|
|
922
|
+
).sub(lockedAmount);
|
|
923
|
+
const floored = variableBalance.isNegative();
|
|
924
|
+
|
|
785
925
|
return {
|
|
786
926
|
...tranche,
|
|
787
927
|
investedAmount: formatEther(
|
|
788
|
-
|
|
789
|
-
lockedAmount,
|
|
790
|
-
),
|
|
928
|
+
floored ? BigNumber.from(0) : variableBalance,
|
|
791
929
|
),
|
|
930
|
+
...(floored
|
|
931
|
+
? { investedAmountFloored: true as const }
|
|
932
|
+
: {}),
|
|
792
933
|
yieldEarnings: {
|
|
793
934
|
...tranche.yieldEarnings,
|
|
794
935
|
lifetime: this.precisionToString(
|
|
@@ -826,7 +967,12 @@ export class Portfolio {
|
|
|
826
967
|
* Cost: three parallel `SystemVariables` reads and the positions query in
|
|
827
968
|
* one round, then one subgraph request per page of the longest series
|
|
828
969
|
* (1000 rows a page) — normally two subgraph round trips in all, never one
|
|
829
|
-
* per epoch.
|
|
970
|
+
* per epoch — alongside one `LendingPool.userBalance` call per pool at
|
|
971
|
+
* the subgraph's block. A pool whose ledger credits more than the wallet
|
|
972
|
+
* holds at that block (shares moved away outside a withdrawal) is left
|
|
973
|
+
* out; if the node says it cannot serve that block, `latest` is read
|
|
974
|
+
* and nothing is. Any other failure of those reads (a rate limit, a
|
|
975
|
+
* network error) THROWS: the history needs a working RPC.
|
|
830
976
|
*
|
|
831
977
|
* On a LITE deployment, a subgraph that lacks the entities (a validation
|
|
832
978
|
* error naming them) yields `[]`; on a full deployment that error throws,
|
|
@@ -867,7 +1013,9 @@ export class Portfolio {
|
|
|
867
1013
|
const userTranches: string[] = [];
|
|
868
1014
|
const tranches = new Set<string>();
|
|
869
1015
|
const locks: string[] = [];
|
|
1016
|
+
const pools: string[] = [];
|
|
870
1017
|
for (const pool of positions.user.lendingPoolUserDetails) {
|
|
1018
|
+
pools.push(pool.lendingPool.id);
|
|
871
1019
|
for (const position of pool.lendingPoolTrancheUserDetails) {
|
|
872
1020
|
userTranches.push(position.id);
|
|
873
1021
|
tranches.add(position.tranche.id);
|
|
@@ -878,26 +1026,311 @@ export class Portfolio {
|
|
|
878
1026
|
}
|
|
879
1027
|
if (userTranches.length === 0) return [];
|
|
880
1028
|
|
|
881
|
-
const { rows } = await
|
|
882
|
-
(
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
1029
|
+
const [{ rows }, chain] = await Promise.all([
|
|
1030
|
+
fetchYieldHistorySeries(
|
|
1031
|
+
(variables) =>
|
|
1032
|
+
this._graph.request<YieldHistoryPageResult>(
|
|
1033
|
+
yieldHistoryPageQuery,
|
|
1034
|
+
variables,
|
|
1035
|
+
),
|
|
1036
|
+
{ pools, tranches: [...tranches], userTranches, locks },
|
|
1037
|
+
window,
|
|
1038
|
+
),
|
|
1039
|
+
this.readChainAt(positions._meta?.block.number, (blockTag) =>
|
|
1040
|
+
this._userLendingService.getUserPoolBalancesAt(
|
|
1041
|
+
userAddress,
|
|
1042
|
+
pools,
|
|
1043
|
+
undefined,
|
|
1044
|
+
blockTag,
|
|
886
1045
|
),
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
1046
|
+
),
|
|
1047
|
+
]);
|
|
1048
|
+
|
|
1049
|
+
// A pool whose ledger credits shares the wallet does not hold at
|
|
1050
|
+
// the same block (shares moved away outside a withdrawal) has no
|
|
1051
|
+
// history we can vouch for: it is left out. Unpinned, nothing is.
|
|
1052
|
+
const decimals = this._kasuConfig.stableAssetDecimals;
|
|
1053
|
+
const withheld = new Set<string>();
|
|
1054
|
+
if (chain.blockTag !== undefined) {
|
|
1055
|
+
positions.user.lendingPoolUserDetails.forEach((pool, index) => {
|
|
1056
|
+
const ledger = pool.lendingPoolTrancheUserDetails.reduce(
|
|
1057
|
+
(sum, position) =>
|
|
1058
|
+
sum +
|
|
1059
|
+
parseFloat(
|
|
1060
|
+
this._userLendingService.convertSharesToAssets(
|
|
1061
|
+
position.shares ?? '0',
|
|
1062
|
+
position.tranche.balance,
|
|
1063
|
+
position.tranche.shares,
|
|
1064
|
+
),
|
|
1065
|
+
),
|
|
1066
|
+
0,
|
|
1067
|
+
);
|
|
1068
|
+
const held = parseFloat(
|
|
1069
|
+
formatUnits(
|
|
1070
|
+
chain.values[index] ?? BigNumber.from(0),
|
|
1071
|
+
decimals,
|
|
1072
|
+
),
|
|
1073
|
+
);
|
|
1074
|
+
if (ledger > held && !ledgerAgrees(ledger, held)) {
|
|
1075
|
+
withheld.add(pool.lendingPool.id.toLowerCase());
|
|
1076
|
+
}
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
890
1079
|
|
|
891
1080
|
return buildYieldHistory({ positions, ...rows }, window, {
|
|
892
1081
|
epochZeroStart: epochZeroStart.toNumber(),
|
|
893
1082
|
epochDuration: epochDuration.toNumber(),
|
|
894
|
-
});
|
|
1083
|
+
}).filter((entry) => !withheld.has(entry.poolId.toLowerCase()));
|
|
895
1084
|
} catch (error) {
|
|
896
1085
|
if (isEmptyHistoryError(error, this._isLiteDeployment)) return [];
|
|
897
1086
|
throw error;
|
|
898
1087
|
}
|
|
899
1088
|
}
|
|
900
1089
|
|
|
1090
|
+
/**
|
|
1091
|
+
* Read the chain at the subgraph's block, so the chain and the ledger
|
|
1092
|
+
* describe the same state: after every clearing the subgraph runs a few
|
|
1093
|
+
* blocks behind, and a `latest` read would then disagree with an
|
|
1094
|
+
* honest ledger (interest moved every balance, deposits were accepted)
|
|
1095
|
+
* and book that difference as a ledger fault.
|
|
1096
|
+
*
|
|
1097
|
+
* Only when the node says it cannot serve that block (pruned, or behind
|
|
1098
|
+
* the subgraph: `isBlockUnavailableError`) does it fall back to `latest`,
|
|
1099
|
+
* and it says so (`blockTag: undefined`): unpinned figures are never
|
|
1100
|
+
* compared with the ledger, and `getPortfolioLendingData` marks the pools
|
|
1101
|
+
* `chainPinned: false`. Any other failure — a rate limit, a timeout, a
|
|
1102
|
+
* network error — is thrown, never turned into an unpinned read.
|
|
1103
|
+
*/
|
|
1104
|
+
private async readChainAt<T>(
|
|
1105
|
+
block: number | undefined,
|
|
1106
|
+
read: (blockTag: number | undefined) => Promise<T>,
|
|
1107
|
+
): Promise<{ values: T; blockTag: number | undefined }> {
|
|
1108
|
+
if (block !== undefined) {
|
|
1109
|
+
try {
|
|
1110
|
+
return { values: await read(block), blockTag: block };
|
|
1111
|
+
} catch (error) {
|
|
1112
|
+
if (!isBlockUnavailableError(error)) throw error;
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
return { values: await read(undefined), blockTag: undefined };
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
/**
|
|
1119
|
+
* One pool's balances: the ledger's (the subgraph counts a lender's
|
|
1120
|
+
* shares as accepted deposits − accepted withdrawals ± top-ups) checked
|
|
1121
|
+
* against the chain's `LendingPool.userBalance` at the SAME block.
|
|
1122
|
+
*
|
|
1123
|
+
* The ledger does not follow shares that move between wallets any other
|
|
1124
|
+
* way (an administrative move of a position is the case seen on chain):
|
|
1125
|
+
* the receiving wallet goes negative as it withdraws, the sending one
|
|
1126
|
+
* keeps shares it no longer holds. Where the two disagree at that block —
|
|
1127
|
+
* or any ledger balance is negative — the tranche balances are read from
|
|
1128
|
+
* the chain at that block (`userActiveAssets`, one call per tranche), the
|
|
1129
|
+
* tranche's ledger `yieldEarned` moves by the same difference, and the
|
|
1130
|
+
* pool carries `ledgerMismatch`. Unpinned (the node could not serve the
|
|
1131
|
+
* block), only a negative ledger balance is corrected. A pool that agrees
|
|
1132
|
+
* — every ordinary lender — costs nothing extra.
|
|
1133
|
+
*
|
|
1134
|
+
* Lifetime figures stay ledger figures: they cannot tell moved principal
|
|
1135
|
+
* from interest.
|
|
1136
|
+
*/
|
|
1137
|
+
private async reconcilePool(
|
|
1138
|
+
userAddress: string,
|
|
1139
|
+
overview: PoolOverview,
|
|
1140
|
+
details: NonNullable<
|
|
1141
|
+
LendingPortfolioQueryResult['user']
|
|
1142
|
+
>['lendingPoolUserDetails'][number],
|
|
1143
|
+
chainBalance: BigNumber,
|
|
1144
|
+
blockTag: number | undefined,
|
|
1145
|
+
provider?: Provider,
|
|
1146
|
+
): Promise<{
|
|
1147
|
+
chainBalance: BigNumber;
|
|
1148
|
+
lifetime: number;
|
|
1149
|
+
tranches: Map<string, { balance: string; yieldEarned: number }>;
|
|
1150
|
+
mismatch?: LedgerMismatch;
|
|
1151
|
+
}> {
|
|
1152
|
+
const decimals = this._kasuConfig.stableAssetDecimals;
|
|
1153
|
+
const chainTotal = parseFloat(formatUnits(chainBalance, decimals));
|
|
1154
|
+
const lifetime =
|
|
1155
|
+
chainTotal -
|
|
1156
|
+
parseFloat(details.totalAcceptedDeposits) +
|
|
1157
|
+
parseFloat(details.totalAcceptedWithdrawnAmount);
|
|
1158
|
+
|
|
1159
|
+
const tranches = new Map<
|
|
1160
|
+
string,
|
|
1161
|
+
{ balance: string; yieldEarned: number }
|
|
1162
|
+
>();
|
|
1163
|
+
for (const position of details.lendingPoolTrancheUserDetails) {
|
|
1164
|
+
const balance = this._userLendingService.convertSharesToAssets(
|
|
1165
|
+
position.shares,
|
|
1166
|
+
position.tranche.balance,
|
|
1167
|
+
position.tranche.shares,
|
|
1168
|
+
);
|
|
1169
|
+
tranches.set(position.tranche.id, {
|
|
1170
|
+
balance,
|
|
1171
|
+
yieldEarned:
|
|
1172
|
+
parseFloat(balance) -
|
|
1173
|
+
parseFloat(position.totalAcceptedDeposits) +
|
|
1174
|
+
parseFloat(position.totalAcceptedWithdrawnAmount),
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
const ledgerTotal = [...tranches.values()].reduce(
|
|
1179
|
+
(sum, { balance }) => sum + parseFloat(balance),
|
|
1180
|
+
0,
|
|
1181
|
+
);
|
|
1182
|
+
const negative = [...tranches.values()].some(
|
|
1183
|
+
({ balance }) => parseFloat(balance) < 0,
|
|
1184
|
+
);
|
|
1185
|
+
const disagrees =
|
|
1186
|
+
blockTag !== undefined && !ledgerAgrees(ledgerTotal, chainTotal);
|
|
1187
|
+
if (!negative && !disagrees) {
|
|
1188
|
+
return { chainBalance, lifetime, tranches };
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
const trancheIds = [
|
|
1192
|
+
...new Set([
|
|
1193
|
+
...overview.tranches.map(({ id }) => id),
|
|
1194
|
+
...tranches.keys(),
|
|
1195
|
+
]),
|
|
1196
|
+
];
|
|
1197
|
+
const onChain =
|
|
1198
|
+
await this._userLendingService.getUserTrancheActiveAssets(
|
|
1199
|
+
userAddress,
|
|
1200
|
+
trancheIds,
|
|
1201
|
+
provider,
|
|
1202
|
+
blockTag,
|
|
1203
|
+
);
|
|
1204
|
+
|
|
1205
|
+
let phantom = false;
|
|
1206
|
+
const reconciled = new Map<
|
|
1207
|
+
string,
|
|
1208
|
+
{ balance: string; yieldEarned: number }
|
|
1209
|
+
>();
|
|
1210
|
+
for (const [index, trancheId] of trancheIds.entries()) {
|
|
1211
|
+
const fromLedger = tranches.get(trancheId);
|
|
1212
|
+
const ledgerBalance = parseFloat(fromLedger?.balance ?? '0');
|
|
1213
|
+
const balance = formatUnits(
|
|
1214
|
+
onChain[index] ?? BigNumber.from(0),
|
|
1215
|
+
decimals,
|
|
1216
|
+
);
|
|
1217
|
+
if (
|
|
1218
|
+
blockTag !== undefined &&
|
|
1219
|
+
ledgerBalance > parseFloat(balance) &&
|
|
1220
|
+
!ledgerAgrees(ledgerBalance, parseFloat(balance))
|
|
1221
|
+
) {
|
|
1222
|
+
phantom = true;
|
|
1223
|
+
}
|
|
1224
|
+
reconciled.set(trancheId, {
|
|
1225
|
+
balance,
|
|
1226
|
+
yieldEarned:
|
|
1227
|
+
(fromLedger?.yieldEarned ?? 0) +
|
|
1228
|
+
parseFloat(balance) -
|
|
1229
|
+
ledgerBalance,
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
return {
|
|
1234
|
+
chainBalance,
|
|
1235
|
+
lifetime,
|
|
1236
|
+
tranches: reconciled,
|
|
1237
|
+
mismatch: {
|
|
1238
|
+
kind: phantom ? 'ledger-exceeds-chain' : 'chain-exceeds-ledger',
|
|
1239
|
+
ledgerBalance: String(ledgerTotal),
|
|
1240
|
+
chainBalance: String(chainTotal),
|
|
1241
|
+
},
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
/**
|
|
1246
|
+
* The last-epoch figures of positions whose newest interest is OLDER than
|
|
1247
|
+
* the recent window. Pools clear independently, and some clear only when
|
|
1248
|
+
* the originator repays, so a pool's last cycle can be weeks back: it is
|
|
1249
|
+
* priced at THAT epoch, from a history read of just those positions over
|
|
1250
|
+
* just those epochs, through the same `positionEpochYield` — so it equals
|
|
1251
|
+
* the history's newest bar.
|
|
1252
|
+
*
|
|
1253
|
+
* A lender who has since left the tranche (no ledger shares now) gets no
|
|
1254
|
+
* figure: an old pool's last cycle is not theirs to show. Costs nothing
|
|
1255
|
+
* when no position qualifies.
|
|
1256
|
+
*/
|
|
1257
|
+
private async staleLastEpochYields(
|
|
1258
|
+
positions: {
|
|
1259
|
+
poolId: string;
|
|
1260
|
+
position: NonNullable<
|
|
1261
|
+
LendingPortfolioQueryResult['user']
|
|
1262
|
+
>['lendingPoolUserDetails'][number]['lendingPoolTrancheUserDetails'][number];
|
|
1263
|
+
}[],
|
|
1264
|
+
recentEpoch: number,
|
|
1265
|
+
): Promise<Map<string, EpochTrancheYield>> {
|
|
1266
|
+
const stale = positions.flatMap(({ poolId, position }) => {
|
|
1267
|
+
const interest =
|
|
1268
|
+
position.tranche.lendingPoolTrancheEpochInterest?.[0];
|
|
1269
|
+
if (interest?.epochId === undefined) return [];
|
|
1270
|
+
if (!(parseFloat(position.shares) > 0)) return [];
|
|
1271
|
+
const epoch = Number(interest.epochId);
|
|
1272
|
+
return epoch < recentEpoch
|
|
1273
|
+
? [{ poolId, position, epoch, interest }]
|
|
1274
|
+
: [];
|
|
1275
|
+
});
|
|
1276
|
+
const yields = new Map<string, EpochTrancheYield>();
|
|
1277
|
+
if (stale.length === 0) return yields;
|
|
1278
|
+
|
|
1279
|
+
const lockId = (
|
|
1280
|
+
poolId: string,
|
|
1281
|
+
lock: { id?: string; lockId: string },
|
|
1282
|
+
): string => lock.id ?? `${poolId.toLowerCase()}-${lock.lockId}`;
|
|
1283
|
+
|
|
1284
|
+
const { rows } = await fetchYieldHistorySeries(
|
|
1285
|
+
(variables) =>
|
|
1286
|
+
this._graph.request<YieldHistoryPageResult>(
|
|
1287
|
+
yieldHistoryPageQuery,
|
|
1288
|
+
variables,
|
|
1289
|
+
),
|
|
1290
|
+
{
|
|
1291
|
+
pools: [...new Set(stale.map(({ poolId }) => poolId))].map(
|
|
1292
|
+
(id) => id.toLowerCase(),
|
|
1293
|
+
),
|
|
1294
|
+
tranches: [
|
|
1295
|
+
...new Set(
|
|
1296
|
+
stale.map(({ position }) => position.tranche.id),
|
|
1297
|
+
),
|
|
1298
|
+
],
|
|
1299
|
+
userTranches: stale.map(({ position }) => position.id),
|
|
1300
|
+
locks: stale.flatMap(({ poolId, position }) =>
|
|
1301
|
+
position.userLendingPoolTrancheFixedTermDepositLocks.map(
|
|
1302
|
+
(lock) => lockId(poolId, lock),
|
|
1303
|
+
),
|
|
1304
|
+
),
|
|
1305
|
+
},
|
|
1306
|
+
{
|
|
1307
|
+
fromEpoch: Math.min(...stale.map(({ epoch }) => epoch)),
|
|
1308
|
+
toEpoch: Math.max(...stale.map(({ epoch }) => epoch)),
|
|
1309
|
+
},
|
|
1310
|
+
);
|
|
1311
|
+
const starts = clearingStarts(rows.clearings);
|
|
1312
|
+
|
|
1313
|
+
for (const { poolId, position, epoch, interest } of stale) {
|
|
1314
|
+
const series = positionSeriesFrom(rows, {
|
|
1315
|
+
...position,
|
|
1316
|
+
userLendingPoolTrancheFixedTermDepositLocks:
|
|
1317
|
+
position.userLendingPoolTrancheFixedTermDepositLocks.map(
|
|
1318
|
+
(lock) => ({ ...lock, id: lockId(poolId, lock) }),
|
|
1319
|
+
),
|
|
1320
|
+
});
|
|
1321
|
+
yields.set(
|
|
1322
|
+
position.id,
|
|
1323
|
+
positionEpochYield(
|
|
1324
|
+
series,
|
|
1325
|
+
epoch,
|
|
1326
|
+
parseFloat(interest.epochInterestAmount),
|
|
1327
|
+
starts.get(poolId.toLowerCase())?.get(epoch),
|
|
1328
|
+
).yield,
|
|
1329
|
+
);
|
|
1330
|
+
}
|
|
1331
|
+
return yields;
|
|
1332
|
+
}
|
|
1333
|
+
|
|
901
1334
|
/**
|
|
902
1335
|
* A money figure as a plain decimal string. Below a millionth of a token
|
|
903
1336
|
* (the stable asset's own precision) is float noise, never money, so it
|