@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
package/dist/bundle.cjs.js
CHANGED
|
@@ -15951,6 +15951,54 @@ function isMissingEntityError(error) {
|
|
|
15951
15951
|
function isEmptyHistoryError(error, liteDeployment) {
|
|
15952
15952
|
return liteDeployment && isMissingEntityError(error);
|
|
15953
15953
|
}
|
|
15954
|
+
/**
|
|
15955
|
+
* What node implementations and RPC providers answer when they cannot serve
|
|
15956
|
+
* state at a requested block: pruned history, or a block they have not seen
|
|
15957
|
+
* yet. Matched on the error's text, lowercased.
|
|
15958
|
+
*/
|
|
15959
|
+
const BLOCK_UNAVAILABLE_PATTERNS = [
|
|
15960
|
+
'missing trie node',
|
|
15961
|
+
'header not found',
|
|
15962
|
+
'unknown block',
|
|
15963
|
+
'block not found',
|
|
15964
|
+
'pruned',
|
|
15965
|
+
'historical state',
|
|
15966
|
+
'state is not available',
|
|
15967
|
+
'state not available',
|
|
15968
|
+
'missing state',
|
|
15969
|
+
'state unavailable',
|
|
15970
|
+
'state histories',
|
|
15971
|
+
'block number out of range',
|
|
15972
|
+
'beyond current head',
|
|
15973
|
+
'is in the future',
|
|
15974
|
+
];
|
|
15975
|
+
function errorTexts(error, depth = 0) {
|
|
15976
|
+
if (depth > 4 || error === null || error === undefined)
|
|
15977
|
+
return [];
|
|
15978
|
+
if (typeof error === 'string')
|
|
15979
|
+
return [error];
|
|
15980
|
+
if (typeof error !== 'object')
|
|
15981
|
+
return [];
|
|
15982
|
+
const record = error;
|
|
15983
|
+
return [
|
|
15984
|
+
'message',
|
|
15985
|
+
'reason',
|
|
15986
|
+
'body',
|
|
15987
|
+
'error',
|
|
15988
|
+
'data',
|
|
15989
|
+
'serverError',
|
|
15990
|
+
].flatMap((key) => errorTexts(record[key], depth + 1));
|
|
15991
|
+
}
|
|
15992
|
+
/**
|
|
15993
|
+
* True when a failed RPC read says the node cannot serve state at the block
|
|
15994
|
+
* asked for (pruned, or not reached yet) — and ONLY then. A rate limit, a
|
|
15995
|
+
* timeout, a refused connection or a revert is not this, and must surface as
|
|
15996
|
+
* the error it is.
|
|
15997
|
+
*/
|
|
15998
|
+
function isBlockUnavailableError(error) {
|
|
15999
|
+
const text = errorTexts(error).join(' ').toLowerCase();
|
|
16000
|
+
return BLOCK_UNAVAILABLE_PATTERNS.some((pattern) => text.includes(pattern));
|
|
16001
|
+
}
|
|
15954
16002
|
|
|
15955
16003
|
/**
|
|
15956
16004
|
* The error a CMS-only call raises on a deployment configured without
|
|
@@ -18080,101 +18128,6 @@ class KSULocking {
|
|
|
18080
18128
|
}
|
|
18081
18129
|
}
|
|
18082
18130
|
|
|
18083
|
-
/**
|
|
18084
|
-
* Settled interest per cycle (epoch): what a lender was credited at each close,
|
|
18085
|
-
* per pool and tranche, and the one formula that turns a tranche's credited
|
|
18086
|
-
* interest into one lender's share of it.
|
|
18087
|
-
*
|
|
18088
|
-
* **The figures are NET, and only net.** `LendingPool._applyTrancheInterest`
|
|
18089
|
-
* takes the performance fee off first and emits `InterestApplied` with the
|
|
18090
|
-
* lender side only (`interest × (1 − performanceFee)`); a fixed-term top-up
|
|
18091
|
-
* (`_applyFixedRateInterests`) is net of the fee the same way. So the figure
|
|
18092
|
-
* this module computes from those records IS what the lender was credited —
|
|
18093
|
-
* never take the fee off it again. No gross is offered: the subgraph keeps
|
|
18094
|
-
* only TODAY's fee, so a historical gross would be a guess.
|
|
18095
|
-
*
|
|
18096
|
-
* Units: amounts are display units of the DEPLOYMENT'S OWN stable token (USDC
|
|
18097
|
-
* on Base and XDC USDC, AUDD on XDC AUDD) — never converted to USD here;
|
|
18098
|
-
* shares are display units of tranche shares, as the subgraph's BigDecimals.
|
|
18099
|
-
*/
|
|
18100
|
-
/**
|
|
18101
|
-
* The ONE per-epoch formula. `Portfolio.getPortfolioLendingData` runs it for
|
|
18102
|
-
* the last settled epoch and `getYieldHistory` for every epoch of a window, so
|
|
18103
|
-
* the two figures cannot drift apart.
|
|
18104
|
-
*
|
|
18105
|
-
* ```
|
|
18106
|
-
* base = epochInterest × userShares / trancheShares
|
|
18107
|
-
* lockBase = base × lock.sharesBeforeEpoch / userShares (active locks)
|
|
18108
|
-
* topUp = (lock.sharesAtEpoch − lock.sharesBeforeEpoch) × trancheBalanceNow / trancheSharesNow
|
|
18109
|
-
* variable = base − Σ lockBase
|
|
18110
|
-
* total = variable + Σ (lockBase + topUp)
|
|
18111
|
-
* ```
|
|
18112
|
-
*
|
|
18113
|
-
* A zero denominator contributes 0 (a tranche or a lender with no shares
|
|
18114
|
-
* earned nothing at that close), never `NaN`.
|
|
18115
|
-
*/
|
|
18116
|
-
function epochTrancheYield(input) {
|
|
18117
|
-
const base = input.trancheShares === 0
|
|
18118
|
-
? 0
|
|
18119
|
-
: (input.epochInterest * input.userShares) / input.trancheShares;
|
|
18120
|
-
let variable = base;
|
|
18121
|
-
const locks = input.locks.map((lock) => {
|
|
18122
|
-
if (!lock.active)
|
|
18123
|
-
return 0;
|
|
18124
|
-
const topUp = input.trancheSharesNow === 0
|
|
18125
|
-
? 0
|
|
18126
|
-
: ((lock.sharesAtEpoch - lock.sharesBeforeEpoch) *
|
|
18127
|
-
input.trancheBalanceNow) /
|
|
18128
|
-
input.trancheSharesNow;
|
|
18129
|
-
const lockBase = input.userShares === 0
|
|
18130
|
-
? 0
|
|
18131
|
-
: (base * lock.sharesBeforeEpoch) / input.userShares;
|
|
18132
|
-
variable -= lockBase;
|
|
18133
|
-
return lockBase + topUp;
|
|
18134
|
-
});
|
|
18135
|
-
const total = locks.reduce((sum, lockYield) => sum + lockYield, variable);
|
|
18136
|
-
return { total, variable, locks };
|
|
18137
|
-
}
|
|
18138
|
-
/**
|
|
18139
|
-
* Pool/tranche entries → one point per epoch, ascending by `epochId`. With no
|
|
18140
|
-
* filter it sums every strategy (the Lend tab's bars); with `poolId` it is one
|
|
18141
|
-
* strategy (a strategy page's bars).
|
|
18142
|
-
*
|
|
18143
|
-
* An epoch appears only if a kept entry has it — there is no zero padding, so
|
|
18144
|
-
* an empty input (a new lender) is an empty output.
|
|
18145
|
-
*
|
|
18146
|
-
* For a fixed-term lock holder the bars can sum to slightly MORE than the
|
|
18147
|
-
* lender's lifetime interest (up to about 0.2 %): each top-up is valued at the
|
|
18148
|
-
* tranche's CURRENT share price, which has grown since. Show "earned to date"
|
|
18149
|
-
* from the lifetime figure, never from this sum.
|
|
18150
|
-
*/
|
|
18151
|
-
function aggregateYieldHistory(entries, filter = {}) {
|
|
18152
|
-
var _a, _b;
|
|
18153
|
-
const poolId = (_a = filter.poolId) === null || _a === void 0 ? void 0 : _a.toLowerCase();
|
|
18154
|
-
const trancheId = (_b = filter.trancheId) === null || _b === void 0 ? void 0 : _b.toLowerCase();
|
|
18155
|
-
const byEpoch = new Map();
|
|
18156
|
-
for (const entry of entries) {
|
|
18157
|
-
if (poolId !== undefined && entry.poolId.toLowerCase() !== poolId) {
|
|
18158
|
-
continue;
|
|
18159
|
-
}
|
|
18160
|
-
if (trancheId !== undefined &&
|
|
18161
|
-
entry.trancheId.toLowerCase() !== trancheId) {
|
|
18162
|
-
continue;
|
|
18163
|
-
}
|
|
18164
|
-
const point = byEpoch.get(entry.epochId);
|
|
18165
|
-
if (point) {
|
|
18166
|
-
point.netYield += entry.netYield;
|
|
18167
|
-
continue;
|
|
18168
|
-
}
|
|
18169
|
-
byEpoch.set(entry.epochId, {
|
|
18170
|
-
epochId: entry.epochId,
|
|
18171
|
-
epochEnd: entry.epochEnd,
|
|
18172
|
-
netYield: entry.netYield,
|
|
18173
|
-
});
|
|
18174
|
-
}
|
|
18175
|
-
return [...byEpoch.values()].sort((a, b) => a.epochId - b.epochId);
|
|
18176
|
-
}
|
|
18177
|
-
|
|
18178
18131
|
/**
|
|
18179
18132
|
* Check if the SDK is configured for a Lite deployment.
|
|
18180
18133
|
* Lite deployments don't have KSU token, locking, or loyalty features.
|
|
@@ -18852,6 +18805,30 @@ class UserLending {
|
|
|
18852
18805
|
});
|
|
18853
18806
|
});
|
|
18854
18807
|
}
|
|
18808
|
+
/**
|
|
18809
|
+
* The lender's ACTIVE assets in each tranche, read from the chain
|
|
18810
|
+
* (`LendingPoolTranche.userActiveAssets`), in the order of `trancheIds`,
|
|
18811
|
+
* in the stable token's base units. Active shares are the ones the lender
|
|
18812
|
+
* owns in the tranche — fixed-term-locked and withdrawal-pending shares
|
|
18813
|
+
* included — whatever path brought them there; `LendingPool.userBalance`
|
|
18814
|
+
* is their sum. One call per tranche.
|
|
18815
|
+
*/
|
|
18816
|
+
getUserTrancheActiveAssets(user, trancheIds, provider, blockTag) {
|
|
18817
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
18818
|
+
return yield Promise.all(trancheIds.map((trancheId) => ILendingPoolTrancheAbi__factory.connect(trancheId, provider !== null && provider !== void 0 ? provider : this._signerOrProvider).userActiveAssets(user, { blockTag: blockTag !== null && blockTag !== void 0 ? blockTag : 'latest' })));
|
|
18819
|
+
});
|
|
18820
|
+
}
|
|
18821
|
+
/**
|
|
18822
|
+
* The lender's balance in each pool (`LendingPool.userBalance`, the sum
|
|
18823
|
+
* of its tranches' active assets), in the order of `poolIds`, in the
|
|
18824
|
+
* stable token's base units, at `blockTag` (default `latest`). One call
|
|
18825
|
+
* per pool.
|
|
18826
|
+
*/
|
|
18827
|
+
getUserPoolBalancesAt(user, poolIds, provider, blockTag) {
|
|
18828
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
18829
|
+
return yield Promise.all(poolIds.map((poolId) => ILendingPoolAbi__factory.connect(poolId, provider !== null && provider !== void 0 ? provider : this._signerOrProvider).userBalance(user, { blockTag: blockTag !== null && blockTag !== void 0 ? blockTag : 'latest' })));
|
|
18830
|
+
});
|
|
18831
|
+
}
|
|
18855
18832
|
getPortfolioUserTrancheBalances(user) {
|
|
18856
18833
|
return __awaiter(this, void 0, void 0, function* () {
|
|
18857
18834
|
const userDetails = yield this._graph.request(portfolioUserTrancheDetailsQuery, {
|
|
@@ -19003,20 +18980,47 @@ gql `
|
|
|
19003
18980
|
}
|
|
19004
18981
|
}
|
|
19005
18982
|
`;
|
|
18983
|
+
/**
|
|
18984
|
+
* The portfolio read. Its last-epoch figures pair each tranche's newest
|
|
18985
|
+
* interest (epoch E) with the shares in force when clearing E applied it —
|
|
18986
|
+
* the selection `getYieldHistory` makes (`positionEpochYield`). For that it
|
|
18987
|
+
* reads, per lender and tranche, the newest share snapshot tagged before
|
|
18988
|
+
* `$recentEpoch` and EVERY snapshot tagged from it on (with their times), and,
|
|
18989
|
+
* for the listed pools (`$poolIds`) and tranches (`$trancheIds`), the
|
|
18990
|
+
* clearings and share exits of those recent epochs. `$recentEpoch` is six
|
|
18991
|
+
* epochs back (`RECENT_PAIRING_EPOCHS`); a newest interest older than that (a
|
|
18992
|
+
* pool that clears rarely) is priced from a history read of its own. Lock
|
|
18993
|
+
* snapshots are read the same way from one epoch further back
|
|
18994
|
+
* (`$lockRecentEpoch`), since a top-up compares E with E − 1. Every list read
|
|
18995
|
+
* with `$first` must come back SHORT of it — a full one is refused, never
|
|
18996
|
+
* used truncated. `_meta` is the block the answer describes: the chain is
|
|
18997
|
+
* read at that same block.
|
|
18998
|
+
*/
|
|
19006
18999
|
const lendingPortfolioQuery = gql `
|
|
19007
19000
|
query LastEpochQuery(
|
|
19008
19001
|
$userAddress: ID!
|
|
19009
19002
|
$epochId: BigInt!
|
|
19010
19003
|
$lastEpochId: BigInt!
|
|
19004
|
+
$recentEpoch: BigInt!
|
|
19005
|
+
$lockRecentEpoch: BigInt!
|
|
19011
19006
|
$unusedPools: [String!]!
|
|
19007
|
+
$poolIds: [String!]!
|
|
19008
|
+
$trancheIds: [String!]!
|
|
19012
19009
|
$first: Int!
|
|
19013
19010
|
) {
|
|
19011
|
+
_meta {
|
|
19012
|
+
block {
|
|
19013
|
+
number
|
|
19014
|
+
}
|
|
19015
|
+
}
|
|
19014
19016
|
user(id: $userAddress) {
|
|
19015
19017
|
lendingPoolUserDetails(
|
|
19016
19018
|
first: $first
|
|
19017
19019
|
where: { lendingPool_not_in: $unusedPools }
|
|
19018
19020
|
) {
|
|
19019
19021
|
id
|
|
19022
|
+
totalAcceptedDeposits
|
|
19023
|
+
totalAcceptedWithdrawnAmount
|
|
19020
19024
|
lendingPool {
|
|
19021
19025
|
id
|
|
19022
19026
|
name
|
|
@@ -19026,16 +19030,34 @@ const lendingPortfolioQuery = gql `
|
|
|
19026
19030
|
}
|
|
19027
19031
|
}
|
|
19028
19032
|
lendingPoolTrancheUserDetails(first: $first) {
|
|
19033
|
+
id
|
|
19029
19034
|
shares
|
|
19035
|
+
totalAcceptedDeposits
|
|
19036
|
+
totalAcceptedWithdrawnAmount
|
|
19030
19037
|
lendingPoolTrancheUserEpochSharesUpdates(
|
|
19031
19038
|
orderDirection: desc
|
|
19032
19039
|
orderBy: shareUpdatesIndex
|
|
19033
|
-
where: { epochId_lt: $
|
|
19040
|
+
where: { epochId_lt: $recentEpoch }
|
|
19034
19041
|
first: 1
|
|
19035
19042
|
) {
|
|
19043
|
+
epochId
|
|
19044
|
+
shareUpdatesIndex
|
|
19036
19045
|
shares
|
|
19046
|
+
timestamp
|
|
19047
|
+
}
|
|
19048
|
+
recentUserShareUpdates: lendingPoolTrancheUserEpochSharesUpdates(
|
|
19049
|
+
orderDirection: desc
|
|
19050
|
+
orderBy: shareUpdatesIndex
|
|
19051
|
+
where: { epochId_gte: $recentEpoch }
|
|
19052
|
+
first: $first
|
|
19053
|
+
) {
|
|
19054
|
+
epochId
|
|
19055
|
+
shareUpdatesIndex
|
|
19056
|
+
shares
|
|
19057
|
+
timestamp
|
|
19037
19058
|
}
|
|
19038
19059
|
userLendingPoolTrancheFixedTermDepositLocks(first: $first) {
|
|
19060
|
+
id
|
|
19039
19061
|
isLocked
|
|
19040
19062
|
unlockAmount
|
|
19041
19063
|
initialAmount
|
|
@@ -19067,6 +19089,26 @@ const lendingPortfolioQuery = gql `
|
|
|
19067
19089
|
) {
|
|
19068
19090
|
shares
|
|
19069
19091
|
}
|
|
19092
|
+
lockSharesBase: userLendingPoolTrancheFixedTermDepositLockShareUpdate(
|
|
19093
|
+
orderBy: shareUpdatesIndex
|
|
19094
|
+
orderDirection: desc
|
|
19095
|
+
first: 1
|
|
19096
|
+
where: { epochId_lt: $lockRecentEpoch }
|
|
19097
|
+
) {
|
|
19098
|
+
epochId
|
|
19099
|
+
shareUpdatesIndex
|
|
19100
|
+
shares
|
|
19101
|
+
}
|
|
19102
|
+
lockSharesRecent: userLendingPoolTrancheFixedTermDepositLockShareUpdate(
|
|
19103
|
+
orderBy: shareUpdatesIndex
|
|
19104
|
+
orderDirection: desc
|
|
19105
|
+
first: $first
|
|
19106
|
+
where: { epochId_gte: $lockRecentEpoch }
|
|
19107
|
+
) {
|
|
19108
|
+
epochId
|
|
19109
|
+
shareUpdatesIndex
|
|
19110
|
+
shares
|
|
19111
|
+
}
|
|
19070
19112
|
}
|
|
19071
19113
|
tranche {
|
|
19072
19114
|
id
|
|
@@ -19076,9 +19118,23 @@ const lendingPortfolioQuery = gql `
|
|
|
19076
19118
|
first: 1
|
|
19077
19119
|
orderBy: shareUpdatesIndex
|
|
19078
19120
|
orderDirection: desc
|
|
19079
|
-
where: { epochId_lt: $
|
|
19121
|
+
where: { epochId_lt: $recentEpoch }
|
|
19080
19122
|
) {
|
|
19123
|
+
epochId
|
|
19124
|
+
shareUpdatesIndex
|
|
19081
19125
|
shares
|
|
19126
|
+
timestamp
|
|
19127
|
+
}
|
|
19128
|
+
recentTrancheShareUpdates: lendingPoolTrancheShareUpdates(
|
|
19129
|
+
first: $first
|
|
19130
|
+
orderBy: shareUpdatesIndex
|
|
19131
|
+
orderDirection: desc
|
|
19132
|
+
where: { epochId_gte: $recentEpoch }
|
|
19133
|
+
) {
|
|
19134
|
+
epochId
|
|
19135
|
+
shareUpdatesIndex
|
|
19136
|
+
shares
|
|
19137
|
+
timestamp
|
|
19082
19138
|
}
|
|
19083
19139
|
lendingPoolTrancheEpochInterest(
|
|
19084
19140
|
first: 1
|
|
@@ -19086,12 +19142,50 @@ const lendingPortfolioQuery = gql `
|
|
|
19086
19142
|
orderDirection: desc
|
|
19087
19143
|
where: { epochId_lt: $epochId }
|
|
19088
19144
|
) {
|
|
19145
|
+
epochId
|
|
19089
19146
|
epochInterestAmount
|
|
19090
19147
|
}
|
|
19091
19148
|
}
|
|
19092
19149
|
}
|
|
19093
19150
|
}
|
|
19094
19151
|
}
|
|
19152
|
+
lendingPoolClearings(
|
|
19153
|
+
first: $first
|
|
19154
|
+
orderBy: id
|
|
19155
|
+
orderDirection: asc
|
|
19156
|
+
where: {
|
|
19157
|
+
lendingPool_in: $poolIds
|
|
19158
|
+
epochId_gte: $recentEpoch
|
|
19159
|
+
epochId_lt: $epochId
|
|
19160
|
+
}
|
|
19161
|
+
) {
|
|
19162
|
+
id
|
|
19163
|
+
epochId
|
|
19164
|
+
startTimestamp
|
|
19165
|
+
lendingPool {
|
|
19166
|
+
id
|
|
19167
|
+
}
|
|
19168
|
+
}
|
|
19169
|
+
recentWithdrawalsAccepted: userRequestEvents(
|
|
19170
|
+
first: $first
|
|
19171
|
+
orderBy: id
|
|
19172
|
+
orderDirection: asc
|
|
19173
|
+
where: {
|
|
19174
|
+
type_in: [WithdrawalAccepted, ImmediateWithdrawalExecuted]
|
|
19175
|
+
tranche_in: $trancheIds
|
|
19176
|
+
epochId_gte: $recentEpoch
|
|
19177
|
+
epochId_lt: $epochId
|
|
19178
|
+
}
|
|
19179
|
+
) {
|
|
19180
|
+
id
|
|
19181
|
+
type
|
|
19182
|
+
epochId
|
|
19183
|
+
createdOn
|
|
19184
|
+
sharesAmount
|
|
19185
|
+
tranche {
|
|
19186
|
+
id
|
|
19187
|
+
}
|
|
19188
|
+
}
|
|
19095
19189
|
userRequests(
|
|
19096
19190
|
first: $first
|
|
19097
19191
|
where: {
|
|
@@ -19119,6 +19213,131 @@ const lendingPortfolioQuery = gql `
|
|
|
19119
19213
|
}
|
|
19120
19214
|
`;
|
|
19121
19215
|
|
|
19216
|
+
/**
|
|
19217
|
+
* Settled interest per cycle (epoch): what a lender was credited at each close,
|
|
19218
|
+
* per pool and tranche, and the one formula that turns a tranche's credited
|
|
19219
|
+
* interest into one lender's share of it.
|
|
19220
|
+
*
|
|
19221
|
+
* **The figures are NET, and only net.** `LendingPool._applyTrancheInterest`
|
|
19222
|
+
* takes the performance fee off first and emits `InterestApplied` with the
|
|
19223
|
+
* lender side only (`interest × (1 − performanceFee)`); a fixed-term top-up
|
|
19224
|
+
* (`_applyFixedRateInterests`) is net of the fee the same way. So the figure
|
|
19225
|
+
* this module computes from those records IS what the lender was credited —
|
|
19226
|
+
* never take the fee off it again. No gross is offered: the subgraph keeps
|
|
19227
|
+
* only TODAY's fee, so a historical gross would be a guess.
|
|
19228
|
+
*
|
|
19229
|
+
* Units: amounts are display units of the DEPLOYMENT'S OWN stable token (USDC
|
|
19230
|
+
* on Base and XDC USDC, AUDD on XDC AUDD) — never converted to USD here;
|
|
19231
|
+
* shares are display units of tranche shares, as the subgraph's BigDecimals.
|
|
19232
|
+
*/
|
|
19233
|
+
/**
|
|
19234
|
+
* The largest change of a lock's shares, as a fraction of its shares before
|
|
19235
|
+
* the epoch, that one clearing's fixed-term top-up can make. The top-up is
|
|
19236
|
+
* the fixed rate's difference from the variable one on one week's balance
|
|
19237
|
+
* (`LendingPool._applyFixedRateInterests`) — a fraction of a percent at any
|
|
19238
|
+
* real rate — so 5 % is far above every genuine top-up and far below any
|
|
19239
|
+
* accounting fault. A lock-share change outside it is NOT interest: it is
|
|
19240
|
+
* left out of the yield (a share movement is never booked as yield). The one
|
|
19241
|
+
* seen on chain is a lock whose recorded shares were set to zero at a
|
|
19242
|
+
* clearing while its shares stayed in the fixed-term contract.
|
|
19243
|
+
*/
|
|
19244
|
+
const MAX_TOP_UP_SHARE_FRACTION = 0.05;
|
|
19245
|
+
/**
|
|
19246
|
+
* True when `sharesBefore → sharesAt` is a change one clearing's fixed-term
|
|
19247
|
+
* top-up can make (`MAX_TOP_UP_SHARE_FRACTION`). A lock with no shares
|
|
19248
|
+
* before has no top-up at all.
|
|
19249
|
+
*/
|
|
19250
|
+
function isPlausibleTopUp(sharesBefore, sharesAt) {
|
|
19251
|
+
if (!(sharesBefore > 0))
|
|
19252
|
+
return sharesAt === sharesBefore;
|
|
19253
|
+
return (Math.abs(sharesAt - sharesBefore) <=
|
|
19254
|
+
sharesBefore * MAX_TOP_UP_SHARE_FRACTION);
|
|
19255
|
+
}
|
|
19256
|
+
/**
|
|
19257
|
+
* The ONE per-epoch formula. `Portfolio.getPortfolioLendingData` runs it for
|
|
19258
|
+
* the last settled epoch and `getYieldHistory` for every epoch of a window, so
|
|
19259
|
+
* the two figures cannot drift apart.
|
|
19260
|
+
*
|
|
19261
|
+
* ```
|
|
19262
|
+
* base = epochInterest × userShares / trancheShares
|
|
19263
|
+
* lockBase = base × lock.sharesBeforeEpoch / userShares (active locks)
|
|
19264
|
+
* topUp = (lock.sharesAtEpoch − lock.sharesBeforeEpoch) × trancheBalanceNow / trancheSharesNow
|
|
19265
|
+
* variable = base − Σ lockBase
|
|
19266
|
+
* total = variable + Σ (lockBase + topUp)
|
|
19267
|
+
* ```
|
|
19268
|
+
*
|
|
19269
|
+
* A zero denominator contributes 0 (a tranche or a lender with no shares
|
|
19270
|
+
* earned nothing at that close), never `NaN`. So does a NEGATIVE share count:
|
|
19271
|
+
* the subgraph's per-lender shares are a ledger of accepted deposits and
|
|
19272
|
+
* withdrawals, and a wallet whose shares arrived some other way (moved to it
|
|
19273
|
+
* from another wallet) withdraws more than the ledger ever credited it. That
|
|
19274
|
+
* position is not one the ledger can describe, and its negative count is not
|
|
19275
|
+
* a negative yield. A lock-share change no top-up can make
|
|
19276
|
+
* (`isPlausibleTopUp`) contributes no top-up.
|
|
19277
|
+
*/
|
|
19278
|
+
function epochTrancheYield(input) {
|
|
19279
|
+
const base = input.trancheShares > 0 && input.userShares > 0
|
|
19280
|
+
? (input.epochInterest * input.userShares) / input.trancheShares
|
|
19281
|
+
: 0;
|
|
19282
|
+
let variable = base;
|
|
19283
|
+
const locks = input.locks.map((lock) => {
|
|
19284
|
+
if (!lock.active)
|
|
19285
|
+
return 0;
|
|
19286
|
+
const topUp = input.trancheSharesNow > 0 &&
|
|
19287
|
+
isPlausibleTopUp(lock.sharesBeforeEpoch, lock.sharesAtEpoch)
|
|
19288
|
+
? ((lock.sharesAtEpoch - lock.sharesBeforeEpoch) *
|
|
19289
|
+
input.trancheBalanceNow) /
|
|
19290
|
+
input.trancheSharesNow
|
|
19291
|
+
: 0;
|
|
19292
|
+
const lockBase = input.userShares > 0
|
|
19293
|
+
? (base * lock.sharesBeforeEpoch) / input.userShares
|
|
19294
|
+
: 0;
|
|
19295
|
+
variable -= lockBase;
|
|
19296
|
+
return lockBase + topUp;
|
|
19297
|
+
});
|
|
19298
|
+
const total = locks.reduce((sum, lockYield) => sum + lockYield, variable);
|
|
19299
|
+
return { total, variable, locks };
|
|
19300
|
+
}
|
|
19301
|
+
/**
|
|
19302
|
+
* Pool/tranche entries → one point per epoch, ascending by `epochId`. With no
|
|
19303
|
+
* filter it sums every strategy (the Lend tab's bars); with `poolId` it is one
|
|
19304
|
+
* strategy (a strategy page's bars).
|
|
19305
|
+
*
|
|
19306
|
+
* An epoch appears only if a kept entry has it — there is no zero padding, so
|
|
19307
|
+
* an empty input (a new lender) is an empty output.
|
|
19308
|
+
*
|
|
19309
|
+
* For a fixed-term lock holder the bars can sum to slightly MORE than the
|
|
19310
|
+
* lender's lifetime interest (up to about 0.2 %): each top-up is valued at the
|
|
19311
|
+
* tranche's CURRENT share price, which has grown since. Show "earned to date"
|
|
19312
|
+
* from the lifetime figure, never from this sum.
|
|
19313
|
+
*/
|
|
19314
|
+
function aggregateYieldHistory(entries, filter = {}) {
|
|
19315
|
+
var _a, _b;
|
|
19316
|
+
const poolId = (_a = filter.poolId) === null || _a === void 0 ? void 0 : _a.toLowerCase();
|
|
19317
|
+
const trancheId = (_b = filter.trancheId) === null || _b === void 0 ? void 0 : _b.toLowerCase();
|
|
19318
|
+
const byEpoch = new Map();
|
|
19319
|
+
for (const entry of entries) {
|
|
19320
|
+
if (poolId !== undefined && entry.poolId.toLowerCase() !== poolId) {
|
|
19321
|
+
continue;
|
|
19322
|
+
}
|
|
19323
|
+
if (trancheId !== undefined &&
|
|
19324
|
+
entry.trancheId.toLowerCase() !== trancheId) {
|
|
19325
|
+
continue;
|
|
19326
|
+
}
|
|
19327
|
+
const point = byEpoch.get(entry.epochId);
|
|
19328
|
+
if (point) {
|
|
19329
|
+
point.netYield += entry.netYield;
|
|
19330
|
+
continue;
|
|
19331
|
+
}
|
|
19332
|
+
byEpoch.set(entry.epochId, {
|
|
19333
|
+
epochId: entry.epochId,
|
|
19334
|
+
epochEnd: entry.epochEnd,
|
|
19335
|
+
netYield: entry.netYield,
|
|
19336
|
+
});
|
|
19337
|
+
}
|
|
19338
|
+
return [...byEpoch.values()].sort((a, b) => a.epochId - b.epochId);
|
|
19339
|
+
}
|
|
19340
|
+
|
|
19122
19341
|
/** The subgraph returns at most this many rows per collection per request. */
|
|
19123
19342
|
const YIELD_HISTORY_PAGE_SIZE = 1000;
|
|
19124
19343
|
/**
|
|
@@ -19182,6 +19401,7 @@ const yieldHistoryPositionsQuery = gql `
|
|
|
19182
19401
|
}
|
|
19183
19402
|
lendingPoolTrancheUserDetails(first: $first) {
|
|
19184
19403
|
id
|
|
19404
|
+
shares
|
|
19185
19405
|
tranche {
|
|
19186
19406
|
id
|
|
19187
19407
|
balance
|
|
@@ -19196,32 +19416,50 @@ const yieldHistoryPositionsQuery = gql `
|
|
|
19196
19416
|
}
|
|
19197
19417
|
}
|
|
19198
19418
|
}
|
|
19419
|
+
_meta {
|
|
19420
|
+
block {
|
|
19421
|
+
number
|
|
19422
|
+
}
|
|
19423
|
+
}
|
|
19199
19424
|
}
|
|
19200
19425
|
`;
|
|
19201
19426
|
/**
|
|
19202
|
-
* Round trip 2 onwards: the
|
|
19427
|
+
* Round trip 2 onwards: the six per-epoch series, one page of each per
|
|
19203
19428
|
* request, cursor-paginated by `id`. A series that is exhausted is skipped with
|
|
19204
19429
|
* `@include`, so a history costs as many requests as its LONGEST series has
|
|
19205
19430
|
* pages — normally one — never one per epoch.
|
|
19206
19431
|
*
|
|
19207
19432
|
* Share snapshots are read up to the window's end with no lower bound: the
|
|
19208
|
-
* value AS OF the first epoch may come from an update long before it.
|
|
19433
|
+
* value AS OF the first epoch may come from an update long before it. Each
|
|
19434
|
+
* carries its `timestamp`, and the window's clearings their start, because a
|
|
19435
|
+
* cycle's interest is paired with the shares in force when the clearing
|
|
19436
|
+
* APPLIED it (`sharesAtInterest`), which the epoch tag alone gets wrong for a
|
|
19437
|
+
* withdrawal accepted between clearings — so the lender's updates are read one
|
|
19438
|
+
* tag past the window (`$userSharesToEpoch`), where the subgraph puts it.
|
|
19439
|
+
* Every share exit between clearings is read too (`trancheSharesAtInterest`
|
|
19440
|
+
* rebuilds the tranche total from the previous clearing's snapshot).
|
|
19209
19441
|
*/
|
|
19210
19442
|
const yieldHistoryPageQuery = gql `
|
|
19211
19443
|
query YieldHistoryPage(
|
|
19444
|
+
$pools: [String!]!
|
|
19212
19445
|
$tranches: [String!]!
|
|
19213
19446
|
$userTranches: [String!]!
|
|
19214
19447
|
$locks: [String!]!
|
|
19215
19448
|
$fromEpoch: BigInt!
|
|
19216
19449
|
$toEpoch: BigInt!
|
|
19450
|
+
$userSharesToEpoch: BigInt!
|
|
19217
19451
|
$interestCursor: ID!
|
|
19218
19452
|
$trancheSharesCursor: ID!
|
|
19219
19453
|
$userSharesCursor: ID!
|
|
19220
19454
|
$lockSharesCursor: ID!
|
|
19455
|
+
$clearingsCursor: ID!
|
|
19456
|
+
$withdrawalsCursor: ID!
|
|
19221
19457
|
$withInterest: Boolean!
|
|
19222
19458
|
$withTrancheShares: Boolean!
|
|
19223
19459
|
$withUserShares: Boolean!
|
|
19224
19460
|
$withLockShares: Boolean!
|
|
19461
|
+
$withClearings: Boolean!
|
|
19462
|
+
$withWithdrawals: Boolean!
|
|
19225
19463
|
$first: Int!
|
|
19226
19464
|
) {
|
|
19227
19465
|
interest: lendingPoolTrancheEpochInterests(
|
|
@@ -19256,6 +19494,7 @@ const yieldHistoryPageQuery = gql `
|
|
|
19256
19494
|
epochId
|
|
19257
19495
|
shareUpdatesIndex
|
|
19258
19496
|
shares
|
|
19497
|
+
timestamp
|
|
19259
19498
|
lendingPoolTranche {
|
|
19260
19499
|
id
|
|
19261
19500
|
}
|
|
@@ -19267,13 +19506,14 @@ const yieldHistoryPageQuery = gql `
|
|
|
19267
19506
|
where: {
|
|
19268
19507
|
id_gt: $userSharesCursor
|
|
19269
19508
|
lendingPoolTrancheUserDetails_in: $userTranches
|
|
19270
|
-
epochId_lte: $
|
|
19509
|
+
epochId_lte: $userSharesToEpoch
|
|
19271
19510
|
}
|
|
19272
19511
|
) @include(if: $withUserShares) {
|
|
19273
19512
|
id
|
|
19274
19513
|
epochId
|
|
19275
19514
|
shareUpdatesIndex
|
|
19276
19515
|
shares
|
|
19516
|
+
timestamp
|
|
19277
19517
|
lendingPoolTrancheUserDetails {
|
|
19278
19518
|
id
|
|
19279
19519
|
}
|
|
@@ -19296,10 +19536,49 @@ const yieldHistoryPageQuery = gql `
|
|
|
19296
19536
|
id
|
|
19297
19537
|
}
|
|
19298
19538
|
}
|
|
19539
|
+
clearings: lendingPoolClearings(
|
|
19540
|
+
first: $first
|
|
19541
|
+
orderBy: id
|
|
19542
|
+
orderDirection: asc
|
|
19543
|
+
where: {
|
|
19544
|
+
id_gt: $clearingsCursor
|
|
19545
|
+
lendingPool_in: $pools
|
|
19546
|
+
epochId_gte: $fromEpoch
|
|
19547
|
+
epochId_lte: $toEpoch
|
|
19548
|
+
}
|
|
19549
|
+
) @include(if: $withClearings) {
|
|
19550
|
+
id
|
|
19551
|
+
epochId
|
|
19552
|
+
startTimestamp
|
|
19553
|
+
lendingPool {
|
|
19554
|
+
id
|
|
19555
|
+
}
|
|
19556
|
+
}
|
|
19557
|
+
withdrawals: userRequestEvents(
|
|
19558
|
+
first: $first
|
|
19559
|
+
orderBy: id
|
|
19560
|
+
orderDirection: asc
|
|
19561
|
+
where: {
|
|
19562
|
+
id_gt: $withdrawalsCursor
|
|
19563
|
+
type_in: [WithdrawalAccepted, ImmediateWithdrawalExecuted]
|
|
19564
|
+
tranche_in: $tranches
|
|
19565
|
+
epochId_gte: $fromEpoch
|
|
19566
|
+
epochId_lte: $toEpoch
|
|
19567
|
+
}
|
|
19568
|
+
) @include(if: $withWithdrawals) {
|
|
19569
|
+
id
|
|
19570
|
+
type
|
|
19571
|
+
epochId
|
|
19572
|
+
createdOn
|
|
19573
|
+
sharesAmount
|
|
19574
|
+
tranche {
|
|
19575
|
+
id
|
|
19576
|
+
}
|
|
19577
|
+
}
|
|
19299
19578
|
}
|
|
19300
19579
|
`;
|
|
19301
19580
|
/**
|
|
19302
|
-
* Fetch the
|
|
19581
|
+
* Fetch the six series for the window, paginating each by `id` until a short
|
|
19303
19582
|
* page. Returns the rows and how many requests it took.
|
|
19304
19583
|
*/
|
|
19305
19584
|
function fetchYieldHistorySeries(request, ids, window) {
|
|
@@ -19310,6 +19589,8 @@ function fetchYieldHistorySeries(request, ids, window) {
|
|
|
19310
19589
|
trancheShares: [],
|
|
19311
19590
|
userShares: [],
|
|
19312
19591
|
lockShares: [],
|
|
19592
|
+
clearings: [],
|
|
19593
|
+
withdrawals: [],
|
|
19313
19594
|
};
|
|
19314
19595
|
const series = [
|
|
19315
19596
|
{ key: 'interest', flag: 'withInterest', cursor: 'interestCursor' },
|
|
@@ -19328,18 +19609,32 @@ function fetchYieldHistorySeries(request, ids, window) {
|
|
|
19328
19609
|
flag: 'withLockShares',
|
|
19329
19610
|
cursor: 'lockSharesCursor',
|
|
19330
19611
|
},
|
|
19612
|
+
{
|
|
19613
|
+
key: 'clearings',
|
|
19614
|
+
flag: 'withClearings',
|
|
19615
|
+
cursor: 'clearingsCursor',
|
|
19616
|
+
},
|
|
19617
|
+
{
|
|
19618
|
+
key: 'withdrawals',
|
|
19619
|
+
flag: 'withWithdrawals',
|
|
19620
|
+
cursor: 'withdrawalsCursor',
|
|
19621
|
+
},
|
|
19331
19622
|
];
|
|
19332
19623
|
const cursors = {
|
|
19333
19624
|
interest: '',
|
|
19334
19625
|
trancheShares: '',
|
|
19335
19626
|
userShares: '',
|
|
19336
19627
|
lockShares: '',
|
|
19628
|
+
clearings: '',
|
|
19629
|
+
withdrawals: '',
|
|
19337
19630
|
};
|
|
19338
19631
|
const open = {
|
|
19339
19632
|
interest: ids.tranches.length > 0,
|
|
19340
19633
|
trancheShares: ids.tranches.length > 0,
|
|
19341
19634
|
userShares: ids.userTranches.length > 0,
|
|
19342
19635
|
lockShares: ids.locks.length > 0,
|
|
19636
|
+
clearings: ids.pools.length > 0,
|
|
19637
|
+
withdrawals: ids.tranches.length > 0,
|
|
19343
19638
|
};
|
|
19344
19639
|
let requests = 0;
|
|
19345
19640
|
while (series.some(({ key }) => open[key])) {
|
|
@@ -19347,11 +19642,15 @@ function fetchYieldHistorySeries(request, ids, window) {
|
|
|
19347
19642
|
throw new Error(`getYieldHistory: more than ${String(YIELD_HISTORY_MAX_PAGES)} pages for one lender`);
|
|
19348
19643
|
}
|
|
19349
19644
|
const variables = {
|
|
19645
|
+
pools: ids.pools,
|
|
19350
19646
|
tranches: ids.tranches,
|
|
19351
19647
|
userTranches: ids.userTranches,
|
|
19352
19648
|
locks: ids.locks,
|
|
19353
19649
|
fromEpoch: String(window.fromEpoch),
|
|
19354
19650
|
toEpoch: String(window.toEpoch),
|
|
19651
|
+
// One tag further for the lender: a withdrawal accepted between
|
|
19652
|
+
// clearings is tagged with the epoch AFTER the one it counts in.
|
|
19653
|
+
userSharesToEpoch: String(window.toEpoch + 1),
|
|
19355
19654
|
first: YIELD_HISTORY_PAGE_SIZE,
|
|
19356
19655
|
};
|
|
19357
19656
|
for (const { key, flag, cursor } of series) {
|
|
@@ -19380,21 +19679,159 @@ function fetchYieldHistorySeries(request, ids, window) {
|
|
|
19380
19679
|
});
|
|
19381
19680
|
}
|
|
19382
19681
|
/**
|
|
19383
|
-
* The snapshot in force at `epoch`: among updates with `epochId ≤ epoch`, the
|
|
19384
|
-
* one with the highest `shareUpdatesIndex` — exactly what
|
|
19385
|
-
* `lendingPortfolioQuery` selects with `orderBy: shareUpdatesIndex, desc,
|
|
19386
|
-
* first: 1, where: { epochId_lt: epoch + 1 }`. `undefined` when none.
|
|
19682
|
+
* The snapshot in force at `epoch`: among updates with `epochId ≤ epoch`, the
|
|
19683
|
+
* one with the highest `shareUpdatesIndex` — exactly what
|
|
19684
|
+
* `lendingPortfolioQuery` selects with `orderBy: shareUpdatesIndex, desc,
|
|
19685
|
+
* first: 1, where: { epochId_lt: epoch + 1 }`. `undefined` when none.
|
|
19686
|
+
*/
|
|
19687
|
+
function sharesAsOf(updates, epoch) {
|
|
19688
|
+
let found;
|
|
19689
|
+
for (const update of updates) {
|
|
19690
|
+
if (Number(update.epochId) > epoch)
|
|
19691
|
+
continue;
|
|
19692
|
+
if (!found || update.shareUpdatesIndex > found.shareUpdatesIndex) {
|
|
19693
|
+
found = update;
|
|
19694
|
+
}
|
|
19695
|
+
}
|
|
19696
|
+
return found;
|
|
19697
|
+
}
|
|
19698
|
+
/**
|
|
19699
|
+
* The snapshot in force when the clearing of `epoch` APPLIED that epoch's
|
|
19700
|
+
* interest — the shares the interest was actually spread over.
|
|
19701
|
+
*
|
|
19702
|
+
* A clearing applies interest FIRST (`ClearingCoordinator.doClearing`: the
|
|
19703
|
+
* `applyInterests` call before steps 1–5), then accepts deposits and
|
|
19704
|
+
* withdrawals. So an update is in force for epoch E's interest when it is
|
|
19705
|
+
* tagged `≤ E` (the subgraph tags every update written during clearing E, or
|
|
19706
|
+
* after it, `E + 1` or later), OR when it was written before clearing E began
|
|
19707
|
+
* (`timestamp < clearingStart`). The second test is the one that matters: a
|
|
19708
|
+
* withdrawal the pool manager accepts BETWEEN clearings
|
|
19709
|
+
* (`forceAcceptWithdrawalRequest`) is tagged `E + 1` by the subgraph, as if a
|
|
19710
|
+
* clearing had accepted it, yet the shares were gone before clearing E.
|
|
19711
|
+
*
|
|
19712
|
+
* Without a clearing start (none recorded) it is `sharesAsOf` — the tag alone.
|
|
19713
|
+
*/
|
|
19714
|
+
function sharesAtInterest(updates, epoch, clearingStart) {
|
|
19715
|
+
let found;
|
|
19716
|
+
for (const update of updates) {
|
|
19717
|
+
const inForce = Number(update.epochId) <= epoch ||
|
|
19718
|
+
(clearingStart !== undefined &&
|
|
19719
|
+
Number(update.timestamp) < clearingStart);
|
|
19720
|
+
if (!inForce)
|
|
19721
|
+
continue;
|
|
19722
|
+
if (!found || update.shareUpdatesIndex > found.shareUpdatesIndex) {
|
|
19723
|
+
found = update;
|
|
19724
|
+
}
|
|
19725
|
+
}
|
|
19726
|
+
return found;
|
|
19727
|
+
}
|
|
19728
|
+
/**
|
|
19729
|
+
* The tranche's total shares when the clearing of `epoch` applied its
|
|
19730
|
+
* interest.
|
|
19731
|
+
*
|
|
19732
|
+
* The subgraph writes a tranche snapshot when a clearing ENDS (tagged with
|
|
19733
|
+
* the next epoch) and at each immediate withdrawal, but none when the manager
|
|
19734
|
+
* accepts a withdrawal between clearings — so the newest snapshot can predate
|
|
19735
|
+
* shares that had already left. Instead of guessing the order of events in a
|
|
19736
|
+
* block, the total is rebuilt from the one snapshot whose place is certain:
|
|
19737
|
+
* the FIRST one tagged `epoch`, written when clearing `epoch − 1` ended (the
|
|
19738
|
+
* tag itself starts there). Every share exit between that and clearing
|
|
19739
|
+
* `epoch` is tagged `epoch` (the pool's clearing epoch at the time), and none
|
|
19740
|
+
* can happen during a clearing (`notPendingClearing`,
|
|
19741
|
+
* `verifyClearingNotPending`), while deposits and top-ups only happen inside
|
|
19742
|
+
* one. So: that snapshot less every exit tagged `epoch` before the clearing's
|
|
19743
|
+
* start (the clearing's own acceptances come at or after it).
|
|
19744
|
+
*
|
|
19745
|
+
* Without a clearing start, or without a snapshot tagged `epoch` (a tranche's
|
|
19746
|
+
* first clearing), it is the snapshot in force by tag alone.
|
|
19747
|
+
*/
|
|
19748
|
+
function trancheSharesAtInterest(snapshots, exits, epoch, clearingStart) {
|
|
19749
|
+
var _a, _b, _c;
|
|
19750
|
+
let base;
|
|
19751
|
+
if (clearingStart !== undefined) {
|
|
19752
|
+
for (const snapshot of snapshots) {
|
|
19753
|
+
if (Number(snapshot.epochId) !== epoch)
|
|
19754
|
+
continue;
|
|
19755
|
+
if (!base || snapshot.shareUpdatesIndex < base.shareUpdatesIndex) {
|
|
19756
|
+
base = snapshot;
|
|
19757
|
+
}
|
|
19758
|
+
}
|
|
19759
|
+
}
|
|
19760
|
+
if (clearingStart === undefined || !base) {
|
|
19761
|
+
return parseFloat((_b = (_a = sharesAsOf(snapshots, epoch)) === null || _a === void 0 ? void 0 : _a.shares) !== null && _b !== void 0 ? _b : '0');
|
|
19762
|
+
}
|
|
19763
|
+
let shares = parseFloat(base.shares);
|
|
19764
|
+
for (const exit of exits) {
|
|
19765
|
+
if (Number(exit.epochId) === epoch &&
|
|
19766
|
+
Number(exit.createdOn) < clearingStart) {
|
|
19767
|
+
shares -= parseFloat((_c = exit.sharesAmount) !== null && _c !== void 0 ? _c : '0');
|
|
19768
|
+
}
|
|
19769
|
+
}
|
|
19770
|
+
return shares;
|
|
19771
|
+
}
|
|
19772
|
+
/**
|
|
19773
|
+
* THE per-position, per-epoch selection both `getYieldHistory` and
|
|
19774
|
+
* `getPortfolioLendingData` run: the shares in force when clearing `epoch`
|
|
19775
|
+
* applied its interest (`sharesAtInterest`, `trancheSharesAtInterest`), each
|
|
19776
|
+
* lock's snapshots as of `epoch` and `epoch − 1`, then `epochTrancheYield`.
|
|
19387
19777
|
*/
|
|
19388
|
-
function
|
|
19389
|
-
|
|
19390
|
-
|
|
19391
|
-
|
|
19778
|
+
function positionEpochYield(series, epoch, epochInterest, clearingStart) {
|
|
19779
|
+
var _a;
|
|
19780
|
+
const userSnapshot = sharesAtInterest(series.userUpdates, epoch, clearingStart);
|
|
19781
|
+
const userShares = parseFloat((_a = userSnapshot === null || userSnapshot === void 0 ? void 0 : userSnapshot.shares) !== null && _a !== void 0 ? _a : '0');
|
|
19782
|
+
const locks = series.locks.map((lock) => {
|
|
19783
|
+
var _a, _b, _c, _d;
|
|
19784
|
+
return ({
|
|
19785
|
+
active: parseFloat(lock.epochLockStart) < epoch &&
|
|
19786
|
+
parseFloat(lock.epochLockEnd) >= epoch &&
|
|
19787
|
+
userSnapshot !== undefined,
|
|
19788
|
+
// No snapshot yet = the lock's opening shares: a lock made after
|
|
19789
|
+
// clearing E starts at E but its first update is tagged E + 1, and
|
|
19790
|
+
// reading that gap as 0 would book its principal as a top-up.
|
|
19791
|
+
sharesAtEpoch: parseFloat((_b = (_a = sharesAsOf(lock.updates, epoch)) === null || _a === void 0 ? void 0 : _a.shares) !== null && _b !== void 0 ? _b : lock.initialTrancheShares),
|
|
19792
|
+
sharesBeforeEpoch: parseFloat((_d = (_c = sharesAsOf(lock.updates, epoch - 1)) === null || _c === void 0 ? void 0 : _c.shares) !== null && _d !== void 0 ? _d : lock.initialTrancheShares),
|
|
19793
|
+
});
|
|
19794
|
+
});
|
|
19795
|
+
return {
|
|
19796
|
+
yield: epochTrancheYield({
|
|
19797
|
+
epochInterest,
|
|
19798
|
+
userShares,
|
|
19799
|
+
trancheShares: trancheSharesAtInterest(series.trancheUpdates, series.exits, epoch, clearingStart),
|
|
19800
|
+
trancheBalanceNow: series.trancheBalanceNow,
|
|
19801
|
+
trancheSharesNow: series.trancheSharesNow,
|
|
19802
|
+
locks,
|
|
19803
|
+
}),
|
|
19804
|
+
userShares,
|
|
19805
|
+
};
|
|
19806
|
+
}
|
|
19807
|
+
/** A position's `PositionSeries` out of the fetched history series. */
|
|
19808
|
+
function positionSeriesFrom(rows, position) {
|
|
19809
|
+
const trancheId = position.tranche.id;
|
|
19810
|
+
return {
|
|
19811
|
+
trancheBalanceNow: parseFloat(position.tranche.balance),
|
|
19812
|
+
trancheSharesNow: parseFloat(position.tranche.shares),
|
|
19813
|
+
userUpdates: rows.userShares.filter((row) => row.lendingPoolTrancheUserDetails.id === position.id),
|
|
19814
|
+
trancheUpdates: rows.trancheShares.filter((row) => row.lendingPoolTranche.id === trancheId),
|
|
19815
|
+
exits: rows.withdrawals.filter((row) => row.tranche.id === trancheId),
|
|
19816
|
+
locks: position.userLendingPoolTrancheFixedTermDepositLocks.map((lock) => (Object.assign(Object.assign({}, lock), { updates: rows.lockShares.filter((row) => row.userLendingPoolTrancheFixedTermDepositLock.id ===
|
|
19817
|
+
lock.id) }))),
|
|
19818
|
+
};
|
|
19819
|
+
}
|
|
19820
|
+
/** `pool → epoch → clearing start`, from the clearing rows. */
|
|
19821
|
+
function clearingStarts(clearings) {
|
|
19822
|
+
var _a;
|
|
19823
|
+
const starts = new Map();
|
|
19824
|
+
for (const clearing of clearings) {
|
|
19825
|
+
const start = Number(clearing.startTimestamp);
|
|
19826
|
+
// A clearing not yet started carries 0: no start to pair by.
|
|
19827
|
+
if (!(start > 0))
|
|
19392
19828
|
continue;
|
|
19393
|
-
|
|
19394
|
-
|
|
19395
|
-
|
|
19829
|
+
const pool = clearing.lendingPool.id.toLowerCase();
|
|
19830
|
+
const byEpoch = (_a = starts.get(pool)) !== null && _a !== void 0 ? _a : new Map();
|
|
19831
|
+
byEpoch.set(Number(clearing.epochId), start);
|
|
19832
|
+
starts.set(pool, byEpoch);
|
|
19396
19833
|
}
|
|
19397
|
-
return
|
|
19834
|
+
return starts;
|
|
19398
19835
|
}
|
|
19399
19836
|
function groupBy(rows, key) {
|
|
19400
19837
|
const map = new Map();
|
|
@@ -19412,12 +19849,13 @@ function groupBy(rows, key) {
|
|
|
19412
19849
|
* Pure: the fetched series → one entry per pool, tranche and settled epoch in
|
|
19413
19850
|
* which the lender held shares (or a lock earned a top-up). Each entry runs the
|
|
19414
19851
|
* same `epochTrancheYield` as the portfolio's last-epoch figure, with the same
|
|
19415
|
-
* share selection, so the
|
|
19852
|
+
* share selection (`sharesAtInterest`, `trancheSharesAtInterest`), so the
|
|
19853
|
+
* newest entry equals that figure.
|
|
19416
19854
|
*
|
|
19417
19855
|
* Sorted by `epochId`, then `poolId`, then `trancheId`.
|
|
19418
19856
|
*/
|
|
19419
19857
|
function buildYieldHistory(read, window, clock) {
|
|
19420
|
-
var _a, _b, _c, _d
|
|
19858
|
+
var _a, _b, _c, _d;
|
|
19421
19859
|
const user = read.positions.user;
|
|
19422
19860
|
if (!user)
|
|
19423
19861
|
return [];
|
|
@@ -19425,45 +19863,36 @@ function buildYieldHistory(read, window, clock) {
|
|
|
19425
19863
|
const trancheSharesByTranche = groupBy(read.trancheShares, (row) => row.lendingPoolTranche.id);
|
|
19426
19864
|
const userSharesByPosition = groupBy(read.userShares, (row) => row.lendingPoolTrancheUserDetails.id);
|
|
19427
19865
|
const lockSharesByLock = groupBy(read.lockShares, (row) => row.userLendingPoolTrancheFixedTermDepositLock.id);
|
|
19866
|
+
const withdrawalsByTranche = groupBy(read.withdrawals, (row) => row.tranche.id);
|
|
19867
|
+
const starts = clearingStarts(read.clearings);
|
|
19428
19868
|
const entries = [];
|
|
19429
19869
|
for (const pool of user.lendingPoolUserDetails) {
|
|
19870
|
+
const poolStarts = starts.get(pool.lendingPool.id.toLowerCase());
|
|
19430
19871
|
for (const position of pool.lendingPoolTrancheUserDetails) {
|
|
19431
19872
|
const trancheId = position.tranche.id;
|
|
19432
19873
|
const trancheUpdates = (_a = trancheSharesByTranche.get(trancheId)) !== null && _a !== void 0 ? _a : [];
|
|
19433
19874
|
const userUpdates = (_b = userSharesByPosition.get(position.id)) !== null && _b !== void 0 ? _b : [];
|
|
19875
|
+
const trancheWithdrawals = (_c = withdrawalsByTranche.get(trancheId)) !== null && _c !== void 0 ? _c : [];
|
|
19434
19876
|
const trancheBalanceNow = parseFloat(position.tranche.balance);
|
|
19435
19877
|
const trancheSharesNow = parseFloat(position.tranche.shares);
|
|
19436
|
-
for (const record of (
|
|
19878
|
+
for (const record of (_d = interestByTranche.get(trancheId)) !== null && _d !== void 0 ? _d : []) {
|
|
19437
19879
|
const epoch = Number(record.epochId);
|
|
19438
19880
|
if (epoch < window.fromEpoch || epoch > window.toEpoch) {
|
|
19439
19881
|
continue;
|
|
19440
19882
|
}
|
|
19441
|
-
const
|
|
19442
|
-
const
|
|
19443
|
-
const locks = position.userLendingPoolTrancheFixedTermDepositLocks.map((lock) => {
|
|
19444
|
-
var _a, _b, _c, _d, _e;
|
|
19445
|
-
const lockUpdates = (_a = lockSharesByLock.get(lock.id)) !== null && _a !== void 0 ? _a : [];
|
|
19446
|
-
return {
|
|
19447
|
-
active: parseFloat(lock.epochLockStart) < epoch &&
|
|
19448
|
-
parseFloat(lock.epochLockEnd) >= epoch &&
|
|
19449
|
-
userSnapshot !== undefined,
|
|
19450
|
-
// No snapshot yet = the lock's opening
|
|
19451
|
-
// shares: a lock made after clearing E
|
|
19452
|
-
// starts at E but its first update is
|
|
19453
|
-
// tagged E + 1, and reading that gap as 0
|
|
19454
|
-
// would book its principal as a top-up.
|
|
19455
|
-
sharesAtEpoch: parseFloat((_c = (_b = sharesAsOf(lockUpdates, epoch)) === null || _b === void 0 ? void 0 : _b.shares) !== null && _c !== void 0 ? _c : lock.initialTrancheShares),
|
|
19456
|
-
sharesBeforeEpoch: parseFloat((_e = (_d = sharesAsOf(lockUpdates, epoch - 1)) === null || _d === void 0 ? void 0 : _d.shares) !== null && _e !== void 0 ? _e : lock.initialTrancheShares),
|
|
19457
|
-
};
|
|
19458
|
-
});
|
|
19459
|
-
const { total } = epochTrancheYield({
|
|
19460
|
-
epochInterest: parseFloat(record.epochInterestAmount),
|
|
19461
|
-
userShares,
|
|
19462
|
-
trancheShares: parseFloat((_f = (_e = sharesAsOf(trancheUpdates, epoch)) === null || _e === void 0 ? void 0 : _e.shares) !== null && _f !== void 0 ? _f : '0'),
|
|
19883
|
+
const clearingStart = poolStarts === null || poolStarts === void 0 ? void 0 : poolStarts.get(epoch);
|
|
19884
|
+
const { yield: epochYield, userShares } = positionEpochYield({
|
|
19463
19885
|
trancheBalanceNow,
|
|
19464
19886
|
trancheSharesNow,
|
|
19465
|
-
|
|
19466
|
-
|
|
19887
|
+
userUpdates,
|
|
19888
|
+
trancheUpdates,
|
|
19889
|
+
exits: trancheWithdrawals,
|
|
19890
|
+
locks: position.userLendingPoolTrancheFixedTermDepositLocks.map((lock) => {
|
|
19891
|
+
var _a;
|
|
19892
|
+
return (Object.assign(Object.assign({}, lock), { updates: (_a = lockSharesByLock.get(lock.id)) !== null && _a !== void 0 ? _a : [] }));
|
|
19893
|
+
}),
|
|
19894
|
+
}, epoch, parseFloat(record.epochInterestAmount), clearingStart);
|
|
19895
|
+
const total = epochYield.total;
|
|
19467
19896
|
// Nothing held and nothing topped up: not part of this
|
|
19468
19897
|
// lender's history (a new lender's history is empty).
|
|
19469
19898
|
if (userShares <= 0 && total === 0)
|
|
@@ -19484,6 +19913,57 @@ function buildYieldHistory(read, window, clock) {
|
|
|
19484
19913
|
a.trancheId.localeCompare(b.trancheId));
|
|
19485
19914
|
}
|
|
19486
19915
|
|
|
19916
|
+
/**
|
|
19917
|
+
* How many epochs back the portfolio read pairs a tranche's newest interest
|
|
19918
|
+
* with the shares in force at its clearing (`lendingPortfolioQuery`): the last
|
|
19919
|
+
* six settled epochs, so a pool whose clearings run late or lag a few weeks
|
|
19920
|
+
* behind the others (Base has one) is paired too. A pool whose newest
|
|
19921
|
+
* interest is older (pools clear independently; some only when the
|
|
19922
|
+
* originator repays) is priced from a history read of its own, at that
|
|
19923
|
+
* epoch. The extra rows are a few per lender and tranche.
|
|
19924
|
+
*/
|
|
19925
|
+
const RECENT_PAIRING_EPOCHS = 6;
|
|
19926
|
+
/**
|
|
19927
|
+
* The ledger (subgraph) and the chain agree on a balance AT THE SAME BLOCK
|
|
19928
|
+
* within a cent, or a millionth of it — the rounding of two share-price
|
|
19929
|
+
* conversions.
|
|
19930
|
+
*/
|
|
19931
|
+
const BALANCE_RECONCILE_TOLERANCE = 0.01;
|
|
19932
|
+
const BALANCE_RECONCILE_RELATIVE = 1e-6;
|
|
19933
|
+
function ledgerAgrees(ledger, chain) {
|
|
19934
|
+
return (Math.abs(ledger - chain) <=
|
|
19935
|
+
Math.max(BALANCE_RECONCILE_TOLERANCE, Math.abs(chain) * BALANCE_RECONCILE_RELATIVE));
|
|
19936
|
+
}
|
|
19937
|
+
/**
|
|
19938
|
+
* A list read with `first: $first` that comes back FULL may have been cut:
|
|
19939
|
+
* refuse it rather than price from a truncated read.
|
|
19940
|
+
*/
|
|
19941
|
+
function refuseTruncated(data) {
|
|
19942
|
+
var _a, _b;
|
|
19943
|
+
const full = (rows) => { var _a; return ((_a = rows === null || rows === void 0 ? void 0 : rows.length) !== null && _a !== void 0 ? _a : 0) >= YIELD_HISTORY_PAGE_SIZE; };
|
|
19944
|
+
const lists = [
|
|
19945
|
+
['lendingPoolClearings', full(data.lendingPoolClearings)],
|
|
19946
|
+
['recentWithdrawalsAccepted', full(data.recentWithdrawalsAccepted)],
|
|
19947
|
+
];
|
|
19948
|
+
for (const pool of (_b = (_a = data.user) === null || _a === void 0 ? void 0 : _a.lendingPoolUserDetails) !== null && _b !== void 0 ? _b : []) {
|
|
19949
|
+
for (const position of pool.lendingPoolTrancheUserDetails) {
|
|
19950
|
+
lists.push([
|
|
19951
|
+
'recentUserShareUpdates',
|
|
19952
|
+
full(position.recentUserShareUpdates),
|
|
19953
|
+
], [
|
|
19954
|
+
'recentTrancheShareUpdates',
|
|
19955
|
+
full(position.tranche.recentTrancheShareUpdates),
|
|
19956
|
+
], ...position.userLendingPoolTrancheFixedTermDepositLocks.map((lock) => [
|
|
19957
|
+
'lockSharesRecent',
|
|
19958
|
+
full(lock.lockSharesRecent),
|
|
19959
|
+
]));
|
|
19960
|
+
}
|
|
19961
|
+
}
|
|
19962
|
+
const cut = lists.find(([, isFull]) => isFull);
|
|
19963
|
+
if (cut) {
|
|
19964
|
+
throw new Error(`getPortfolioLendingData: ${cut[0]} reached ${String(YIELD_HISTORY_PAGE_SIZE)} rows; refusing a possibly truncated read`);
|
|
19965
|
+
}
|
|
19966
|
+
}
|
|
19487
19967
|
class Portfolio {
|
|
19488
19968
|
constructor(_kasuConfig, signerOrProvider) {
|
|
19489
19969
|
this._kasuConfig = _kasuConfig;
|
|
@@ -19701,23 +20181,52 @@ class Portfolio {
|
|
|
19701
20181
|
}
|
|
19702
20182
|
getPortfolioLendingData(userAddress, poolOverviews, currentEpoch, provider) {
|
|
19703
20183
|
return __awaiter(this, void 0, void 0, function* () {
|
|
19704
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
|
|
20184
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r;
|
|
19705
20185
|
const SECONDS_PER_EPOCH = 604800; // seconds in a week
|
|
19706
20186
|
const portfolioLendingPoolsPromise = [];
|
|
19707
20187
|
const previousEpoch = ethers.BigNumber.from(currentEpoch).sub(ethers.BigNumber.from(1));
|
|
19708
|
-
const
|
|
19709
|
-
|
|
19710
|
-
|
|
19711
|
-
|
|
19712
|
-
|
|
19713
|
-
|
|
19714
|
-
|
|
19715
|
-
|
|
19716
|
-
|
|
19717
|
-
this.
|
|
19718
|
-
|
|
20188
|
+
const recentEpoch = Math.max(0, previousEpoch.toNumber() - (RECENT_PAIRING_EPOCHS - 1));
|
|
20189
|
+
const unused = new Set(this._kasuConfig.UNUSED_LENDING_POOL_IDS.map((id) => id.toLowerCase()));
|
|
20190
|
+
const listedPools = poolOverviews.filter(({ id }) => !unused.has(id.toLowerCase()));
|
|
20191
|
+
const lastEpochData = yield this._graph.request(lendingPortfolioQuery, {
|
|
20192
|
+
userAddress: userAddress.toLowerCase(),
|
|
20193
|
+
epochId: parseFloat(currentEpoch),
|
|
20194
|
+
lastEpochId: previousEpoch.toNumber(),
|
|
20195
|
+
recentEpoch,
|
|
20196
|
+
lockRecentEpoch: Math.max(0, recentEpoch - 1),
|
|
20197
|
+
unusedPools: this._kasuConfig.UNUSED_LENDING_POOL_IDS,
|
|
20198
|
+
poolIds: listedPools.map(({ id }) => id.toLowerCase()),
|
|
20199
|
+
trancheIds: listedPools.flatMap(({ tranches }) => tranches.map(({ id }) => id.toLowerCase())),
|
|
20200
|
+
first: YIELD_HISTORY_PAGE_SIZE,
|
|
20201
|
+
});
|
|
19719
20202
|
if (!lastEpochData.user)
|
|
19720
20203
|
return [];
|
|
20204
|
+
refuseTruncated(lastEpochData);
|
|
20205
|
+
// The pools the lender has a ledger in AND that are listed: the only
|
|
20206
|
+
// ones read on chain, and the only ones reported.
|
|
20207
|
+
const userPools = listedPools.flatMap((overview) => {
|
|
20208
|
+
var _a;
|
|
20209
|
+
const details = (_a = lastEpochData.user) === null || _a === void 0 ? void 0 : _a.lendingPoolUserDetails.find(({ lendingPool }) => lendingPool.id.toLowerCase() === overview.id.toLowerCase());
|
|
20210
|
+
return details ? [{ overview, details }] : [];
|
|
20211
|
+
});
|
|
20212
|
+
const recentStarts = clearingStarts(lastEpochData.lendingPoolClearings);
|
|
20213
|
+
const recentExits = new Map();
|
|
20214
|
+
for (const exit of lastEpochData.recentWithdrawalsAccepted) {
|
|
20215
|
+
const list = (_a = recentExits.get(exit.tranche.id)) !== null && _a !== void 0 ? _a : [];
|
|
20216
|
+
list.push(exit);
|
|
20217
|
+
recentExits.set(exit.tranche.id, list);
|
|
20218
|
+
}
|
|
20219
|
+
const [chain, staleYields] = yield Promise.all([
|
|
20220
|
+
this.readChainAt((_b = lastEpochData._meta) === null || _b === void 0 ? void 0 : _b.block.number, (blockTag) => this._userLendingService.getUserPoolBalancesAt(userAddress, userPools.map(({ overview }) => overview.id), provider, blockTag)),
|
|
20221
|
+
this.staleLastEpochYields(userPools.flatMap(({ overview, details }) => details.lendingPoolTrancheUserDetails.map((position) => ({
|
|
20222
|
+
poolId: overview.id,
|
|
20223
|
+
position,
|
|
20224
|
+
}))), recentEpoch),
|
|
20225
|
+
]);
|
|
20226
|
+
const reconciled = yield Promise.all(userPools.map(({ overview, details }, index) => {
|
|
20227
|
+
var _a;
|
|
20228
|
+
return this.reconcilePool(userAddress, overview, details, (_a = chain.values[index]) !== null && _a !== void 0 ? _a : ethers.BigNumber.from(0), chain.blockTag, provider);
|
|
20229
|
+
}));
|
|
19721
20230
|
const userRequestsMap = new Map();
|
|
19722
20231
|
for (const userRequest of lastEpochData.userRequests) {
|
|
19723
20232
|
const trancheId = userRequest.tranche.id;
|
|
@@ -19743,28 +20252,22 @@ class Portfolio {
|
|
|
19743
20252
|
continue;
|
|
19744
20253
|
}
|
|
19745
20254
|
userRequestsMap.set(trancheId, Object.assign(Object.assign({}, mappedUserRequest), { [ftdId]: [
|
|
19746
|
-
...((
|
|
20255
|
+
...((_c = mappedUserRequest[ftdId]) !== null && _c !== void 0 ? _c : []),
|
|
19747
20256
|
userRequestFtdItem,
|
|
19748
20257
|
] }));
|
|
19749
20258
|
}
|
|
19750
20259
|
}
|
|
19751
|
-
|
|
19752
|
-
|
|
19753
|
-
const userPoolBalance = userPoolBalances.find(({ poolId }) => poolId === poolOverview.id);
|
|
19754
|
-
if (!userPoolBalance) {
|
|
19755
|
-
continue;
|
|
19756
|
-
}
|
|
19757
|
-
totalInvestments = userPoolBalance.balance.add(totalInvestments);
|
|
20260
|
+
for (const [poolIndex, { overview: poolOverview, details },] of userPools.entries()) {
|
|
20261
|
+
const pool = reconciled[poolIndex];
|
|
19758
20262
|
const tranches = [];
|
|
19759
20263
|
let poolYieldEarnedLastEpoch = 0;
|
|
19760
|
-
|
|
19761
|
-
|
|
19762
|
-
|
|
19763
|
-
const portfolioUserTrancheBalance = portfolioUserTrancheBalances.get(poolOverview.id);
|
|
20264
|
+
// A ledger crediting shares the wallet does not hold earns
|
|
20265
|
+
// nothing we can vouch for: its last-cycle figures are withheld.
|
|
20266
|
+
const withheld = ((_d = pool.mismatch) === null || _d === void 0 ? void 0 : _d.kind) === 'ledger-exceeds-chain';
|
|
19764
20267
|
for (const tranche of poolOverview.tranches) {
|
|
19765
|
-
const lastEpochUserTrancheDetails =
|
|
20268
|
+
const lastEpochUserTrancheDetails = details.lendingPoolTrancheUserDetails.find((lendingPool) => lendingPool.tranche.id === tranche.id);
|
|
19766
20269
|
const userRequest = userRequestsMap.get(tranche.id);
|
|
19767
|
-
const depositDetails = (
|
|
20270
|
+
const depositDetails = (_e = userRequest === null || userRequest === void 0 ? void 0 : userRequest['0']) !== null && _e !== void 0 ? _e : [];
|
|
19768
20271
|
if (!lastEpochUserTrancheDetails) {
|
|
19769
20272
|
tranches.push(Object.assign(Object.assign({}, tranche), { investedAmount: {
|
|
19770
20273
|
balance: '0',
|
|
@@ -19775,55 +20278,61 @@ class Portfolio {
|
|
|
19775
20278
|
}, depositDetails, fixedLoans: [] }));
|
|
19776
20279
|
continue;
|
|
19777
20280
|
}
|
|
19778
|
-
|
|
19779
|
-
|
|
19780
|
-
|
|
20281
|
+
// The tranche's newest interest (epoch E), priced with the
|
|
20282
|
+
// shares in force when clearing E applied it — the one
|
|
20283
|
+
// selection `getYieldHistory` makes (`positionEpochYield`).
|
|
20284
|
+
// Inside the recent window the candidates below are complete.
|
|
20285
|
+
// An older E (a pool that clears rarely — some only when the
|
|
20286
|
+
// originator repays) is priced from a history read of its own,
|
|
20287
|
+
// and is 0 for a lender who has left the tranche.
|
|
20288
|
+
const newestInterest = (_f = lastEpochUserTrancheDetails.tranche
|
|
20289
|
+
.lendingPoolTrancheEpochInterest) === null || _f === void 0 ? void 0 : _f[0];
|
|
20290
|
+
const interestEpoch = (newestInterest === null || newestInterest === void 0 ? void 0 : newestInterest.epochId) === undefined
|
|
20291
|
+
? undefined
|
|
20292
|
+
: Number(newestInterest.epochId);
|
|
19781
20293
|
const fixedTermDeposits = lastEpochUserTrancheDetails.userLendingPoolTrancheFixedTermDepositLocks;
|
|
19782
|
-
|
|
19783
|
-
|
|
19784
|
-
|
|
19785
|
-
|
|
19786
|
-
epochInterest: parseFloat((_f = (_e = (_d = lastEpochUserTrancheDetails.tranche
|
|
19787
|
-
.lendingPoolTrancheEpochInterest) === null || _d === void 0 ? void 0 : _d[0]) === null || _e === void 0 ? void 0 : _e.epochInterestAmount) !== null && _f !== void 0 ? _f : '0'),
|
|
19788
|
-
userShares: parseFloat((_j = (_h = (_g = lastEpochUserTrancheDetails
|
|
19789
|
-
.lendingPoolTrancheUserEpochSharesUpdates) === null || _g === void 0 ? void 0 : _g[0]) === null || _h === void 0 ? void 0 : _h.shares) !== null && _j !== void 0 ? _j : '0'),
|
|
19790
|
-
trancheShares: parseFloat((_m = (_l = (_k = lastEpochUserTrancheDetails.tranche
|
|
19791
|
-
.lendingPoolTrancheShareUpdates) === null || _k === void 0 ? void 0 : _k[0]) === null || _l === void 0 ? void 0 : _l.shares) !== null && _m !== void 0 ? _m : '0'),
|
|
19792
|
-
trancheBalanceNow: parseFloat(lastEpochUserTrancheDetails.tranche.balance),
|
|
19793
|
-
trancheSharesNow: parseFloat(lastEpochUserTrancheDetails.tranche.shares),
|
|
19794
|
-
locks: fixedTermDeposits.map((fixedTermDeposit) => {
|
|
19795
|
-
var _a, _b, _c, _d, _e, _f;
|
|
19796
|
-
return ({
|
|
19797
|
-
active: parseFloat(fixedTermDeposit.epochLockStart) <
|
|
19798
|
-
lastEpoch &&
|
|
19799
|
-
parseFloat(fixedTermDeposit.epochLockEnd) >=
|
|
19800
|
-
lastEpoch &&
|
|
19801
|
-
userHasLastEpochShares,
|
|
19802
|
-
// No snapshot = the lock's opening shares, exactly
|
|
19803
|
-
// as `getYieldHistory` reads it: a lock made after
|
|
19804
|
-
// clearing E starts at E with its first update
|
|
19805
|
-
// tagged E + 1. Never the CURRENT shares, which
|
|
19806
|
-
// would carry a top-up from a clearing that ran
|
|
19807
|
-
// after the last settled epoch.
|
|
19808
|
-
sharesAtEpoch: parseFloat((_c = (_b = (_a = fixedTermDeposit.lockSharesAtLastEpoch) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.shares) !== null && _c !== void 0 ? _c : fixedTermDeposit.initialTrancheShares),
|
|
19809
|
-
sharesBeforeEpoch: parseFloat((_f = (_e = (_d = fixedTermDeposit
|
|
19810
|
-
.userLendingPoolTrancheFixedTermDepositLockShareUpdate) === null || _d === void 0 ? void 0 : _d[0]) === null || _e === void 0 ? void 0 : _e.shares) !== null && _f !== void 0 ? _f : fixedTermDeposit.initialTrancheShares),
|
|
19811
|
-
});
|
|
19812
|
-
}),
|
|
19813
|
-
});
|
|
19814
|
-
const userTrancheBalance = {
|
|
19815
|
-
balance: '0',
|
|
19816
|
-
yieldEarned: 0,
|
|
20294
|
+
const noYield = {
|
|
20295
|
+
total: 0,
|
|
20296
|
+
variable: 0,
|
|
20297
|
+
locks: fixedTermDeposits.map(() => 0),
|
|
19817
20298
|
};
|
|
19818
|
-
|
|
19819
|
-
|
|
19820
|
-
|
|
19821
|
-
|
|
19822
|
-
|
|
19823
|
-
|
|
19824
|
-
portfolioUserTranche.yieldEarned;
|
|
19825
|
-
}
|
|
20299
|
+
let lastEpochYield = noYield;
|
|
20300
|
+
if (interestEpoch !== undefined &&
|
|
20301
|
+
interestEpoch < recentEpoch &&
|
|
20302
|
+
!withheld) {
|
|
20303
|
+
lastEpochYield =
|
|
20304
|
+
(_g = staleYields.get(lastEpochUserTrancheDetails.id)) !== null && _g !== void 0 ? _g : noYield;
|
|
19826
20305
|
}
|
|
20306
|
+
else if (interestEpoch !== undefined && !withheld) {
|
|
20307
|
+
lastEpochYield = positionEpochYield({
|
|
20308
|
+
trancheBalanceNow: parseFloat(lastEpochUserTrancheDetails.tranche.balance),
|
|
20309
|
+
trancheSharesNow: parseFloat(lastEpochUserTrancheDetails.tranche.shares),
|
|
20310
|
+
userUpdates: [
|
|
20311
|
+
...((_h = lastEpochUserTrancheDetails.lendingPoolTrancheUserEpochSharesUpdates) !== null && _h !== void 0 ? _h : []),
|
|
20312
|
+
...((_j = lastEpochUserTrancheDetails.recentUserShareUpdates) !== null && _j !== void 0 ? _j : []),
|
|
20313
|
+
],
|
|
20314
|
+
trancheUpdates: [
|
|
20315
|
+
...((_k = lastEpochUserTrancheDetails.tranche
|
|
20316
|
+
.lendingPoolTrancheShareUpdates) !== null && _k !== void 0 ? _k : []),
|
|
20317
|
+
...((_l = lastEpochUserTrancheDetails.tranche
|
|
20318
|
+
.recentTrancheShareUpdates) !== null && _l !== void 0 ? _l : []),
|
|
20319
|
+
],
|
|
20320
|
+
exits: (_m = recentExits.get(tranche.id)) !== null && _m !== void 0 ? _m : [],
|
|
20321
|
+
locks: fixedTermDeposits.map((lock) => {
|
|
20322
|
+
var _a, _b;
|
|
20323
|
+
return (Object.assign(Object.assign({}, lock), { updates: [
|
|
20324
|
+
...((_a = lock.lockSharesBase) !== null && _a !== void 0 ? _a : []),
|
|
20325
|
+
...((_b = lock.lockSharesRecent) !== null && _b !== void 0 ? _b : []),
|
|
20326
|
+
] }));
|
|
20327
|
+
}),
|
|
20328
|
+
}, interestEpoch, parseFloat((_o = newestInterest === null || newestInterest === void 0 ? void 0 : newestInterest.epochInterestAmount) !== null && _o !== void 0 ? _o : '0'), (_p = recentStarts
|
|
20329
|
+
.get(poolOverview.id.toLowerCase())) === null || _p === void 0 ? void 0 : _p.get(interestEpoch)).yield;
|
|
20330
|
+
}
|
|
20331
|
+
const ledger = pool.tranches.get(tranche.id);
|
|
20332
|
+
const userTrancheBalance = {
|
|
20333
|
+
balance: (_q = ledger === null || ledger === void 0 ? void 0 : ledger.balance) !== null && _q !== void 0 ? _q : '0',
|
|
20334
|
+
yieldEarned: (_r = ledger === null || ledger === void 0 ? void 0 : ledger.yieldEarned) !== null && _r !== void 0 ? _r : 0,
|
|
20335
|
+
};
|
|
19827
20336
|
let totalLifetimeFtdYield = 0;
|
|
19828
20337
|
const fixedLoans = fixedTermDeposits.map((fixedTermDeposit, lockIndex) => {
|
|
19829
20338
|
var _a, _b;
|
|
@@ -19871,7 +20380,9 @@ class Portfolio {
|
|
|
19871
20380
|
}, fixedLoans,
|
|
19872
20381
|
depositDetails }));
|
|
19873
20382
|
}
|
|
19874
|
-
portfolioLendingPoolsPromise.push(Object.assign(Object.assign({}, poolOverview), { totalYieldEarningsLastEpoch: this.precisionToString(poolYieldEarnedLastEpoch), totalInvestedAmount:
|
|
20383
|
+
portfolioLendingPoolsPromise.push(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, poolOverview), { totalYieldEarningsLastEpoch: this.precisionToString(poolYieldEarnedLastEpoch), totalInvestedAmount: pool.chainBalance.toString(), totalYieldEarningsLifetime: pool.lifetime.toString() }), (pool.mismatch ? { ledgerMismatch: pool.mismatch } : {})), (chain.blockTag === undefined
|
|
20384
|
+
? { chainPinned: false }
|
|
20385
|
+
: {})), { tranches }));
|
|
19875
20386
|
}
|
|
19876
20387
|
const portfolioLendingPools = portfolioLendingPoolsPromise.map((pool) => (Object.assign(Object.assign({}, pool), { tranches: pool.tranches.map((tranche) => {
|
|
19877
20388
|
const trancheBalance = tranche.investedAmount;
|
|
@@ -19882,7 +20393,14 @@ class Portfolio {
|
|
|
19882
20393
|
}, ethers.BigNumber.from(0));
|
|
19883
20394
|
const totalTrancheYieldEarned = trancheBalance.yieldEarned.toFixed(6);
|
|
19884
20395
|
const totalTrancheFtdYieldEarned = parseFloat(tranche.yieldEarnings.lifetime).toFixed(6);
|
|
19885
|
-
|
|
20396
|
+
// Never negative: a variable balance below the locked
|
|
20397
|
+
// amount is a ledger the chain does not back (the pool
|
|
20398
|
+
// carries `ledgerMismatch`), not money owed — flagged.
|
|
20399
|
+
const variableBalance = utils$2.parseEther(trancheBalance.balance).sub(lockedAmount);
|
|
20400
|
+
const floored = variableBalance.isNegative();
|
|
20401
|
+
return Object.assign(Object.assign(Object.assign(Object.assign({}, tranche), { investedAmount: utils$2.formatEther(floored ? ethers.BigNumber.from(0) : variableBalance) }), (floored
|
|
20402
|
+
? { investedAmountFloored: true }
|
|
20403
|
+
: {})), { yieldEarnings: Object.assign(Object.assign({}, tranche.yieldEarnings), { lifetime: this.precisionToString(parseFloat(totalTrancheYieldEarned) -
|
|
19886
20404
|
parseFloat(totalTrancheFtdYieldEarned)) }) });
|
|
19887
20405
|
}) })));
|
|
19888
20406
|
return portfolioLendingPools.filter((pool) => !utils$2.parseUnits(pool.totalInvestedAmount).isZero() ||
|
|
@@ -19908,7 +20426,12 @@ class Portfolio {
|
|
|
19908
20426
|
* Cost: three parallel `SystemVariables` reads and the positions query in
|
|
19909
20427
|
* one round, then one subgraph request per page of the longest series
|
|
19910
20428
|
* (1000 rows a page) — normally two subgraph round trips in all, never one
|
|
19911
|
-
* per epoch.
|
|
20429
|
+
* per epoch — alongside one `LendingPool.userBalance` call per pool at
|
|
20430
|
+
* the subgraph's block. A pool whose ledger credits more than the wallet
|
|
20431
|
+
* holds at that block (shares moved away outside a withdrawal) is left
|
|
20432
|
+
* out; if the node says it cannot serve that block, `latest` is read
|
|
20433
|
+
* and nothing is. Any other failure of those reads (a rate limit, a
|
|
20434
|
+
* network error) THROWS: the history needs a working RPC.
|
|
19912
20435
|
*
|
|
19913
20436
|
* On a LITE deployment, a subgraph that lacks the entities (a validation
|
|
19914
20437
|
* error naming them) yields `[]`; on a full deployment that error throws,
|
|
@@ -19919,6 +20442,7 @@ class Portfolio {
|
|
|
19919
20442
|
*/
|
|
19920
20443
|
getYieldHistory(userAddress_1) {
|
|
19921
20444
|
return __awaiter(this, arguments, void 0, function* (userAddress, options = {}) {
|
|
20445
|
+
var _a;
|
|
19922
20446
|
validateYieldHistoryOptions(options);
|
|
19923
20447
|
try {
|
|
19924
20448
|
const [currentEpoch, epochZeroStart, epochDuration, positions] = yield Promise.all([
|
|
@@ -19937,7 +20461,9 @@ class Portfolio {
|
|
|
19937
20461
|
const userTranches = [];
|
|
19938
20462
|
const tranches = new Set();
|
|
19939
20463
|
const locks = [];
|
|
20464
|
+
const pools = [];
|
|
19940
20465
|
for (const pool of positions.user.lendingPoolUserDetails) {
|
|
20466
|
+
pools.push(pool.lendingPool.id);
|
|
19941
20467
|
for (const position of pool.lendingPoolTrancheUserDetails) {
|
|
19942
20468
|
userTranches.push(position.id);
|
|
19943
20469
|
tranches.add(position.tranche.id);
|
|
@@ -19948,11 +20474,33 @@ class Portfolio {
|
|
|
19948
20474
|
}
|
|
19949
20475
|
if (userTranches.length === 0)
|
|
19950
20476
|
return [];
|
|
19951
|
-
const { rows } = yield
|
|
20477
|
+
const [{ rows }, chain] = yield Promise.all([
|
|
20478
|
+
fetchYieldHistorySeries((variables) => this._graph.request(yieldHistoryPageQuery, variables), { pools, tranches: [...tranches], userTranches, locks }, window),
|
|
20479
|
+
this.readChainAt((_a = positions._meta) === null || _a === void 0 ? void 0 : _a.block.number, (blockTag) => this._userLendingService.getUserPoolBalancesAt(userAddress, pools, undefined, blockTag)),
|
|
20480
|
+
]);
|
|
20481
|
+
// A pool whose ledger credits shares the wallet does not hold at
|
|
20482
|
+
// the same block (shares moved away outside a withdrawal) has no
|
|
20483
|
+
// history we can vouch for: it is left out. Unpinned, nothing is.
|
|
20484
|
+
const decimals = this._kasuConfig.stableAssetDecimals;
|
|
20485
|
+
const withheld = new Set();
|
|
20486
|
+
if (chain.blockTag !== undefined) {
|
|
20487
|
+
positions.user.lendingPoolUserDetails.forEach((pool, index) => {
|
|
20488
|
+
var _a;
|
|
20489
|
+
const ledger = pool.lendingPoolTrancheUserDetails.reduce((sum, position) => {
|
|
20490
|
+
var _a;
|
|
20491
|
+
return sum +
|
|
20492
|
+
parseFloat(this._userLendingService.convertSharesToAssets((_a = position.shares) !== null && _a !== void 0 ? _a : '0', position.tranche.balance, position.tranche.shares));
|
|
20493
|
+
}, 0);
|
|
20494
|
+
const held = parseFloat(utils$2.formatUnits((_a = chain.values[index]) !== null && _a !== void 0 ? _a : ethers.BigNumber.from(0), decimals));
|
|
20495
|
+
if (ledger > held && !ledgerAgrees(ledger, held)) {
|
|
20496
|
+
withheld.add(pool.lendingPool.id.toLowerCase());
|
|
20497
|
+
}
|
|
20498
|
+
});
|
|
20499
|
+
}
|
|
19952
20500
|
return buildYieldHistory(Object.assign({ positions }, rows), window, {
|
|
19953
20501
|
epochZeroStart: epochZeroStart.toNumber(),
|
|
19954
20502
|
epochDuration: epochDuration.toNumber(),
|
|
19955
|
-
});
|
|
20503
|
+
}).filter((entry) => !withheld.has(entry.poolId.toLowerCase()));
|
|
19956
20504
|
}
|
|
19957
20505
|
catch (error) {
|
|
19958
20506
|
if (isEmptyHistoryError(error, this._isLiteDeployment))
|
|
@@ -19961,6 +20509,164 @@ class Portfolio {
|
|
|
19961
20509
|
}
|
|
19962
20510
|
});
|
|
19963
20511
|
}
|
|
20512
|
+
/**
|
|
20513
|
+
* Read the chain at the subgraph's block, so the chain and the ledger
|
|
20514
|
+
* describe the same state: after every clearing the subgraph runs a few
|
|
20515
|
+
* blocks behind, and a `latest` read would then disagree with an
|
|
20516
|
+
* honest ledger (interest moved every balance, deposits were accepted)
|
|
20517
|
+
* and book that difference as a ledger fault.
|
|
20518
|
+
*
|
|
20519
|
+
* Only when the node says it cannot serve that block (pruned, or behind
|
|
20520
|
+
* the subgraph: `isBlockUnavailableError`) does it fall back to `latest`,
|
|
20521
|
+
* and it says so (`blockTag: undefined`): unpinned figures are never
|
|
20522
|
+
* compared with the ledger, and `getPortfolioLendingData` marks the pools
|
|
20523
|
+
* `chainPinned: false`. Any other failure — a rate limit, a timeout, a
|
|
20524
|
+
* network error — is thrown, never turned into an unpinned read.
|
|
20525
|
+
*/
|
|
20526
|
+
readChainAt(block, read) {
|
|
20527
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
20528
|
+
if (block !== undefined) {
|
|
20529
|
+
try {
|
|
20530
|
+
return { values: yield read(block), blockTag: block };
|
|
20531
|
+
}
|
|
20532
|
+
catch (error) {
|
|
20533
|
+
if (!isBlockUnavailableError(error))
|
|
20534
|
+
throw error;
|
|
20535
|
+
}
|
|
20536
|
+
}
|
|
20537
|
+
return { values: yield read(undefined), blockTag: undefined };
|
|
20538
|
+
});
|
|
20539
|
+
}
|
|
20540
|
+
/**
|
|
20541
|
+
* One pool's balances: the ledger's (the subgraph counts a lender's
|
|
20542
|
+
* shares as accepted deposits − accepted withdrawals ± top-ups) checked
|
|
20543
|
+
* against the chain's `LendingPool.userBalance` at the SAME block.
|
|
20544
|
+
*
|
|
20545
|
+
* The ledger does not follow shares that move between wallets any other
|
|
20546
|
+
* way (an administrative move of a position is the case seen on chain):
|
|
20547
|
+
* the receiving wallet goes negative as it withdraws, the sending one
|
|
20548
|
+
* keeps shares it no longer holds. Where the two disagree at that block —
|
|
20549
|
+
* or any ledger balance is negative — the tranche balances are read from
|
|
20550
|
+
* the chain at that block (`userActiveAssets`, one call per tranche), the
|
|
20551
|
+
* tranche's ledger `yieldEarned` moves by the same difference, and the
|
|
20552
|
+
* pool carries `ledgerMismatch`. Unpinned (the node could not serve the
|
|
20553
|
+
* block), only a negative ledger balance is corrected. A pool that agrees
|
|
20554
|
+
* — every ordinary lender — costs nothing extra.
|
|
20555
|
+
*
|
|
20556
|
+
* Lifetime figures stay ledger figures: they cannot tell moved principal
|
|
20557
|
+
* from interest.
|
|
20558
|
+
*/
|
|
20559
|
+
reconcilePool(userAddress, overview, details, chainBalance, blockTag, provider) {
|
|
20560
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
20561
|
+
var _a, _b, _c;
|
|
20562
|
+
const decimals = this._kasuConfig.stableAssetDecimals;
|
|
20563
|
+
const chainTotal = parseFloat(utils$2.formatUnits(chainBalance, decimals));
|
|
20564
|
+
const lifetime = chainTotal -
|
|
20565
|
+
parseFloat(details.totalAcceptedDeposits) +
|
|
20566
|
+
parseFloat(details.totalAcceptedWithdrawnAmount);
|
|
20567
|
+
const tranches = new Map();
|
|
20568
|
+
for (const position of details.lendingPoolTrancheUserDetails) {
|
|
20569
|
+
const balance = this._userLendingService.convertSharesToAssets(position.shares, position.tranche.balance, position.tranche.shares);
|
|
20570
|
+
tranches.set(position.tranche.id, {
|
|
20571
|
+
balance,
|
|
20572
|
+
yieldEarned: parseFloat(balance) -
|
|
20573
|
+
parseFloat(position.totalAcceptedDeposits) +
|
|
20574
|
+
parseFloat(position.totalAcceptedWithdrawnAmount),
|
|
20575
|
+
});
|
|
20576
|
+
}
|
|
20577
|
+
const ledgerTotal = [...tranches.values()].reduce((sum, { balance }) => sum + parseFloat(balance), 0);
|
|
20578
|
+
const negative = [...tranches.values()].some(({ balance }) => parseFloat(balance) < 0);
|
|
20579
|
+
const disagrees = blockTag !== undefined && !ledgerAgrees(ledgerTotal, chainTotal);
|
|
20580
|
+
if (!negative && !disagrees) {
|
|
20581
|
+
return { chainBalance, lifetime, tranches };
|
|
20582
|
+
}
|
|
20583
|
+
const trancheIds = [
|
|
20584
|
+
...new Set([
|
|
20585
|
+
...overview.tranches.map(({ id }) => id),
|
|
20586
|
+
...tranches.keys(),
|
|
20587
|
+
]),
|
|
20588
|
+
];
|
|
20589
|
+
const onChain = yield this._userLendingService.getUserTrancheActiveAssets(userAddress, trancheIds, provider, blockTag);
|
|
20590
|
+
let phantom = false;
|
|
20591
|
+
const reconciled = new Map();
|
|
20592
|
+
for (const [index, trancheId] of trancheIds.entries()) {
|
|
20593
|
+
const fromLedger = tranches.get(trancheId);
|
|
20594
|
+
const ledgerBalance = parseFloat((_a = fromLedger === null || fromLedger === void 0 ? void 0 : fromLedger.balance) !== null && _a !== void 0 ? _a : '0');
|
|
20595
|
+
const balance = utils$2.formatUnits((_b = onChain[index]) !== null && _b !== void 0 ? _b : ethers.BigNumber.from(0), decimals);
|
|
20596
|
+
if (blockTag !== undefined &&
|
|
20597
|
+
ledgerBalance > parseFloat(balance) &&
|
|
20598
|
+
!ledgerAgrees(ledgerBalance, parseFloat(balance))) {
|
|
20599
|
+
phantom = true;
|
|
20600
|
+
}
|
|
20601
|
+
reconciled.set(trancheId, {
|
|
20602
|
+
balance,
|
|
20603
|
+
yieldEarned: ((_c = fromLedger === null || fromLedger === void 0 ? void 0 : fromLedger.yieldEarned) !== null && _c !== void 0 ? _c : 0) +
|
|
20604
|
+
parseFloat(balance) -
|
|
20605
|
+
ledgerBalance,
|
|
20606
|
+
});
|
|
20607
|
+
}
|
|
20608
|
+
return {
|
|
20609
|
+
chainBalance,
|
|
20610
|
+
lifetime,
|
|
20611
|
+
tranches: reconciled,
|
|
20612
|
+
mismatch: {
|
|
20613
|
+
kind: phantom ? 'ledger-exceeds-chain' : 'chain-exceeds-ledger',
|
|
20614
|
+
ledgerBalance: String(ledgerTotal),
|
|
20615
|
+
chainBalance: String(chainTotal),
|
|
20616
|
+
},
|
|
20617
|
+
};
|
|
20618
|
+
});
|
|
20619
|
+
}
|
|
20620
|
+
/**
|
|
20621
|
+
* The last-epoch figures of positions whose newest interest is OLDER than
|
|
20622
|
+
* the recent window. Pools clear independently, and some clear only when
|
|
20623
|
+
* the originator repays, so a pool's last cycle can be weeks back: it is
|
|
20624
|
+
* priced at THAT epoch, from a history read of just those positions over
|
|
20625
|
+
* just those epochs, through the same `positionEpochYield` — so it equals
|
|
20626
|
+
* the history's newest bar.
|
|
20627
|
+
*
|
|
20628
|
+
* A lender who has since left the tranche (no ledger shares now) gets no
|
|
20629
|
+
* figure: an old pool's last cycle is not theirs to show. Costs nothing
|
|
20630
|
+
* when no position qualifies.
|
|
20631
|
+
*/
|
|
20632
|
+
staleLastEpochYields(positions, recentEpoch) {
|
|
20633
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
20634
|
+
var _a;
|
|
20635
|
+
const stale = positions.flatMap(({ poolId, position }) => {
|
|
20636
|
+
var _a;
|
|
20637
|
+
const interest = (_a = position.tranche.lendingPoolTrancheEpochInterest) === null || _a === void 0 ? void 0 : _a[0];
|
|
20638
|
+
if ((interest === null || interest === void 0 ? void 0 : interest.epochId) === undefined)
|
|
20639
|
+
return [];
|
|
20640
|
+
if (!(parseFloat(position.shares) > 0))
|
|
20641
|
+
return [];
|
|
20642
|
+
const epoch = Number(interest.epochId);
|
|
20643
|
+
return epoch < recentEpoch
|
|
20644
|
+
? [{ poolId, position, epoch, interest }]
|
|
20645
|
+
: [];
|
|
20646
|
+
});
|
|
20647
|
+
const yields = new Map();
|
|
20648
|
+
if (stale.length === 0)
|
|
20649
|
+
return yields;
|
|
20650
|
+
const lockId = (poolId, lock) => { var _a; return (_a = lock.id) !== null && _a !== void 0 ? _a : `${poolId.toLowerCase()}-${lock.lockId}`; };
|
|
20651
|
+
const { rows } = yield fetchYieldHistorySeries((variables) => this._graph.request(yieldHistoryPageQuery, variables), {
|
|
20652
|
+
pools: [...new Set(stale.map(({ poolId }) => poolId))].map((id) => id.toLowerCase()),
|
|
20653
|
+
tranches: [
|
|
20654
|
+
...new Set(stale.map(({ position }) => position.tranche.id)),
|
|
20655
|
+
],
|
|
20656
|
+
userTranches: stale.map(({ position }) => position.id),
|
|
20657
|
+
locks: stale.flatMap(({ poolId, position }) => position.userLendingPoolTrancheFixedTermDepositLocks.map((lock) => lockId(poolId, lock))),
|
|
20658
|
+
}, {
|
|
20659
|
+
fromEpoch: Math.min(...stale.map(({ epoch }) => epoch)),
|
|
20660
|
+
toEpoch: Math.max(...stale.map(({ epoch }) => epoch)),
|
|
20661
|
+
});
|
|
20662
|
+
const starts = clearingStarts(rows.clearings);
|
|
20663
|
+
for (const { poolId, position, epoch, interest } of stale) {
|
|
20664
|
+
const series = positionSeriesFrom(rows, Object.assign(Object.assign({}, position), { userLendingPoolTrancheFixedTermDepositLocks: position.userLendingPoolTrancheFixedTermDepositLocks.map((lock) => (Object.assign(Object.assign({}, lock), { id: lockId(poolId, lock) }))) }));
|
|
20665
|
+
yields.set(position.id, positionEpochYield(series, epoch, parseFloat(interest.epochInterestAmount), (_a = starts.get(poolId.toLowerCase())) === null || _a === void 0 ? void 0 : _a.get(epoch)).yield);
|
|
20666
|
+
}
|
|
20667
|
+
return yields;
|
|
20668
|
+
});
|
|
20669
|
+
}
|
|
19964
20670
|
/**
|
|
19965
20671
|
* A money figure as a plain decimal string. Below a millionth of a token
|
|
19966
20672
|
* (the stable asset's own precision) is float noise, never money, so it
|
|
@@ -27878,19 +28584,30 @@ function buildLoanAgreementSignMessage(p) {
|
|
|
27878
28584
|
].join('\n');
|
|
27879
28585
|
}
|
|
27880
28586
|
/**
|
|
27881
|
-
* The legacy `/contract/generate`
|
|
28587
|
+
* The legacy `/contract/generate` message.
|
|
27882
28588
|
*
|
|
27883
28589
|
* ⚠️ BYTE-EXACT PROTOCOL STRING. kasu-backend rebuilds it as
|
|
27884
28590
|
* `` `I request contract content for ${address} at ${timestamp}.` `` from the
|
|
27885
28591
|
* `address` and `timestamp` fields of the request body — so the string signed
|
|
27886
28592
|
* and the body sent must agree exactly, INCLUDING the address casing. This
|
|
27887
28593
|
* builder lowercases, and the request body must carry the same lowercased
|
|
27888
|
-
* address
|
|
28594
|
+
* address.
|
|
27889
28595
|
*
|
|
27890
28596
|
* The backend takes this path whenever the four human-readable display fields
|
|
27891
|
-
* are absent
|
|
27892
|
-
*
|
|
27893
|
-
* signs
|
|
28597
|
+
* are absent. Only the retired kasu-fe-next app (retired 2026-11-05) and
|
|
28598
|
+
* kasu-mobile builds from before 2026-09-06 signed it; every current consumer
|
|
28599
|
+
* signs `buildLoanAgreementSignMessage`. The backend keeps accepting it until
|
|
28600
|
+
* no such mobile build is left in use.
|
|
28601
|
+
*
|
|
28602
|
+
* It is NOT the `/contract/resolve` message. Resolve has a message of its own
|
|
28603
|
+
* (`Requesting contract content for {address}/{id} at {timestamp}.`), which
|
|
28604
|
+
* kasu-backend rebuilds and verifies, so it does not depend on this format.
|
|
28605
|
+
*
|
|
28606
|
+
* @deprecated Sign the human-readable loan-agreement message
|
|
28607
|
+
* (`buildLoanAgreementSignMessage`, `contractMessage.format:
|
|
28608
|
+
* 'loan-agreement'`) instead. Only the retired kasu-fe-next app and
|
|
28609
|
+
* pre-2026-09-06 kasu-mobile builds used this format; it will be removed in
|
|
28610
|
+
* the next major version.
|
|
27894
28611
|
*
|
|
27895
28612
|
* @param timestampMs ms-epoch, and the same value sent as the body's
|
|
27896
28613
|
* `timestamp`.
|
|
@@ -29309,6 +30026,381 @@ function maxNetRateCeiling(pools, feePercent) {
|
|
|
29309
30026
|
return rate;
|
|
29310
30027
|
}
|
|
29311
30028
|
|
|
30029
|
+
/**
|
|
30030
|
+
* The CMS-authored figures and text a strategy's Details screen shows, read
|
|
30031
|
+
* the way kasu-ui's Details tab reads them, so every app shows the same
|
|
30032
|
+
* thing.
|
|
30033
|
+
*
|
|
30034
|
+
* Lifted from kasu-ui (`lib/details-fields.ts`, `lib/directus-text.ts` and
|
|
30035
|
+
* the parsers inside `deposit-dialog/details-tab.tsx`). The rules are facts
|
|
30036
|
+
* about how the CMS is authored, not presentation:
|
|
30037
|
+
*
|
|
30038
|
+
* - EMPTY IS UNAUTHORED. The CMS stores an emptied field as `''`, and its
|
|
30039
|
+
* "numeric" fields arrive as a number, a numeric string, `''` or `null`.
|
|
30040
|
+
* Anything that is not real data reads as `undefined` — never a `0`, a
|
|
30041
|
+
* placeholder or a sentence of the SDK's own. A field the caller cannot
|
|
30042
|
+
* read is a row the caller omits.
|
|
30043
|
+
* - A START DATE MUST DATE. A value that is not an ISO date, or a real
|
|
30044
|
+
* calendar date, or is before 2000 or in the future, is refused: rendered
|
|
30045
|
+
* as elapsed time it would claim a history nobody authored.
|
|
30046
|
+
* - SOME FIELDS CARRY TWO LINES IN ONE STRING. Business Types and Facility
|
|
30047
|
+
* Type are a primary value and a qualifying clause; risk-item
|
|
30048
|
+
* descriptions can be numbered enumerations. Splitting them is the same
|
|
30049
|
+
* everywhere, so it lives here.
|
|
30050
|
+
*
|
|
30051
|
+
* Like `pickShortDescription`, this authors no string of its own: it selects,
|
|
30052
|
+
* splits and trims text the CMS already holds. None of it is sanitised —
|
|
30053
|
+
* render every string as text, never as HTML or markdown.
|
|
30054
|
+
*
|
|
30055
|
+
* Pure. No clock: the date reader takes `now` from its caller.
|
|
30056
|
+
*/
|
|
30057
|
+
// ---------------------------------------------------------------------------
|
|
30058
|
+
// Start dates
|
|
30059
|
+
// ---------------------------------------------------------------------------
|
|
30060
|
+
/**
|
|
30061
|
+
* `YYYY-MM-DD`, optionally followed by a time part — the two shapes a CMS
|
|
30062
|
+
* date column serialises to. Checked BEFORE any parse: `new Date('Jun 2024')`
|
|
30063
|
+
* is a valid date in V8, so an editorial string would otherwise become a
|
|
30064
|
+
* confident duration.
|
|
30065
|
+
*/
|
|
30066
|
+
const ISO_DATE = /^(\d{4})-(\d{2})-(\d{2})(?:[T ].*)?$/;
|
|
30067
|
+
/** Nothing on Kasu has a lending history from before this year. */
|
|
30068
|
+
const EARLIEST_YEAR = 2000;
|
|
30069
|
+
/**
|
|
30070
|
+
* A CMS start date as its `YYYY-MM-DD` date head, or `undefined`.
|
|
30071
|
+
*
|
|
30072
|
+
* Accepts an ISO date, optionally with a time part (only the date head is
|
|
30073
|
+
* kept — a time changes nothing at year/month granularity, and converting it
|
|
30074
|
+
* through a timezone could move it a day). Refused, as `undefined`:
|
|
30075
|
+
*
|
|
30076
|
+
* - anything that is not a string (or, with `unixSeconds`, a number);
|
|
30077
|
+
* - an empty or blank string (unauthored);
|
|
30078
|
+
* - a string that is not ISO-shaped (`'Jun 2024'`);
|
|
30079
|
+
* - a date that does not exist (`'2024-02-30'`, `'2024-13-01'`) — never
|
|
30080
|
+
* rolled over into the next month;
|
|
30081
|
+
* - a year before 2000;
|
|
30082
|
+
* - a date after `now` (a typo such as 2062 for 2026 — no history has
|
|
30083
|
+
* begun in the future).
|
|
30084
|
+
*
|
|
30085
|
+
* `now` is the caller's clock; the facade passes the current time.
|
|
30086
|
+
*/
|
|
30087
|
+
function readCmsStartDate(value, now, options = {}) {
|
|
30088
|
+
let head;
|
|
30089
|
+
if (typeof value === 'number' && options.unixSeconds) {
|
|
30090
|
+
// `0` is formatDuration's "unset" sentinel; negatives are pre-1970.
|
|
30091
|
+
if (!Number.isFinite(value) || value <= 0)
|
|
30092
|
+
return undefined;
|
|
30093
|
+
const date = new Date(value * 1000);
|
|
30094
|
+
if (Number.isNaN(date.getTime()))
|
|
30095
|
+
return undefined;
|
|
30096
|
+
head = date.toISOString().slice(0, 10);
|
|
30097
|
+
}
|
|
30098
|
+
else if (typeof value === 'string') {
|
|
30099
|
+
const trimmed = value.trim();
|
|
30100
|
+
if (!ISO_DATE.test(trimmed))
|
|
30101
|
+
return undefined;
|
|
30102
|
+
head = trimmed.slice(0, 10);
|
|
30103
|
+
}
|
|
30104
|
+
else {
|
|
30105
|
+
return undefined;
|
|
30106
|
+
}
|
|
30107
|
+
const match = ISO_DATE.exec(head);
|
|
30108
|
+
if (!match)
|
|
30109
|
+
return undefined;
|
|
30110
|
+
const year = Number(match[1]);
|
|
30111
|
+
const month = Number(match[2]);
|
|
30112
|
+
const day = Number(match[3]);
|
|
30113
|
+
// Round-trip through UTC: a day or month out of range comes back as a
|
|
30114
|
+
// different date, which is how a non-existent date is caught.
|
|
30115
|
+
const utc = new Date(Date.UTC(year, month - 1, day));
|
|
30116
|
+
if (utc.getUTCFullYear() !== year ||
|
|
30117
|
+
utc.getUTCMonth() !== month - 1 ||
|
|
30118
|
+
utc.getUTCDate() !== day) {
|
|
30119
|
+
return undefined;
|
|
30120
|
+
}
|
|
30121
|
+
if (year < EARLIEST_YEAR)
|
|
30122
|
+
return undefined;
|
|
30123
|
+
if (utc.getTime() > now.getTime())
|
|
30124
|
+
return undefined;
|
|
30125
|
+
return head;
|
|
30126
|
+
}
|
|
30127
|
+
/**
|
|
30128
|
+
* Business Types (the CMS `industryExposure`), split into the value and its
|
|
30129
|
+
* supporting clause on the FIRST newline, else the FIRST parenthesised
|
|
30130
|
+
* segment (a trailing `)` dropped):
|
|
30131
|
+
*
|
|
30132
|
+
* `'SMEs & Large Businesses (diversified industries – exclusions apply)'`
|
|
30133
|
+
* → `{ primary: 'SMEs & Large Businesses', detail: 'diversified industries – exclusions apply' }`
|
|
30134
|
+
*
|
|
30135
|
+
* A value with neither delimiter is `{ primary }` alone. `undefined` for a
|
|
30136
|
+
* missing, non-string, empty or blank value.
|
|
30137
|
+
*
|
|
30138
|
+
* REFINED over kasu-ui: a value that OPENS with `(` would split to an empty
|
|
30139
|
+
* primary there; here the whole value is the primary instead, so `primary`
|
|
30140
|
+
* is never empty. No live record has that shape.
|
|
30141
|
+
*/
|
|
30142
|
+
function splitBusinessTypes(raw) {
|
|
30143
|
+
const value = typeof raw === 'string' ? raw.trim() : '';
|
|
30144
|
+
if (!value)
|
|
30145
|
+
return undefined;
|
|
30146
|
+
let primary = value;
|
|
30147
|
+
let detail = '';
|
|
30148
|
+
const newlineAt = value.indexOf('\n');
|
|
30149
|
+
if (newlineAt !== -1) {
|
|
30150
|
+
primary = value.slice(0, newlineAt).trim();
|
|
30151
|
+
detail = value.slice(newlineAt + 1).trim();
|
|
30152
|
+
}
|
|
30153
|
+
else {
|
|
30154
|
+
const parenAt = value.indexOf('(');
|
|
30155
|
+
if (parenAt !== -1) {
|
|
30156
|
+
primary = value.slice(0, parenAt).trim();
|
|
30157
|
+
detail = value.slice(parenAt + 1).trim();
|
|
30158
|
+
if (detail.endsWith(')'))
|
|
30159
|
+
detail = detail.slice(0, -1).trim();
|
|
30160
|
+
}
|
|
30161
|
+
}
|
|
30162
|
+
if (!primary)
|
|
30163
|
+
return { primary: value };
|
|
30164
|
+
return detail ? { primary, detail } : { primary };
|
|
30165
|
+
}
|
|
30166
|
+
/**
|
|
30167
|
+
* The start of the label, or a character that is neither a word character
|
|
30168
|
+
* nor a hyphen, before `revolv` — not `\b`, which treats a hyphen as a
|
|
30169
|
+
* boundary, so "Non-Revolving" would match. kasu-ui writes the same rule as
|
|
30170
|
+
* the lookbehind `(?<![\w-])`; this form needs no lookbehind support from the
|
|
30171
|
+
* JavaScript engine.
|
|
30172
|
+
*/
|
|
30173
|
+
const REVOLVING_FACILITY = /(?:^|[^\w-])revolv\w*\b/i;
|
|
30174
|
+
/**
|
|
30175
|
+
* Facility Type (the CMS `loanStructure`): the label on the FIRST line, and
|
|
30176
|
+
* everything after the first newline as its clause, so a clause with line
|
|
30177
|
+
* breaks of its own stays whole.
|
|
30178
|
+
*
|
|
30179
|
+
* `'Revolving Instalment Facility\n12-24 months instalment loan, subject to annual review'`
|
|
30180
|
+
* → `{ label: 'Revolving Instalment Facility', detail: '12-24 months …', revolving: true }`
|
|
30181
|
+
*
|
|
30182
|
+
* `undefined` for a missing, non-string, empty or blank value. The value is
|
|
30183
|
+
* trimmed first, so a leading blank line still yields a label.
|
|
30184
|
+
*/
|
|
30185
|
+
function parseFacility(raw) {
|
|
30186
|
+
const value = typeof raw === 'string' ? raw.trim() : '';
|
|
30187
|
+
if (!value)
|
|
30188
|
+
return undefined;
|
|
30189
|
+
const breakAt = value.indexOf('\n');
|
|
30190
|
+
const label = (breakAt === -1 ? value : value.slice(0, breakAt)).trim();
|
|
30191
|
+
const detail = breakAt === -1 ? '' : value.slice(breakAt + 1).trim();
|
|
30192
|
+
const revolving = REVOLVING_FACILITY.test(label);
|
|
30193
|
+
return detail ? { label, detail, revolving } : { label, revolving };
|
|
30194
|
+
}
|
|
30195
|
+
// ---------------------------------------------------------------------------
|
|
30196
|
+
// Risk Management
|
|
30197
|
+
// ---------------------------------------------------------------------------
|
|
30198
|
+
/**
|
|
30199
|
+
* The order the Risk Management groups read in, by group name (compared
|
|
30200
|
+
* trimmed, case-insensitively). The CMS returns items in authoring order,
|
|
30201
|
+
* which is not this. A group not listed still appears — after these, in CMS
|
|
30202
|
+
* order.
|
|
30203
|
+
*/
|
|
30204
|
+
const RISK_GROUP_ORDER = Object.freeze([
|
|
30205
|
+
'Security & Recourse',
|
|
30206
|
+
'Structural Controls',
|
|
30207
|
+
'Financial Covenants',
|
|
30208
|
+
'Reporting Covenants',
|
|
30209
|
+
'Key Eligibility Criteria',
|
|
30210
|
+
]);
|
|
30211
|
+
/** Zero-width paste artifacts, which `\s` does not match. */
|
|
30212
|
+
const ZERO_WIDTH = /[-]/g;
|
|
30213
|
+
/** An `N)` marker at the start, or after a `;` / `; and,` separator. */
|
|
30214
|
+
const HAS_ENUMERATION = /(?:^\d+\)|;\s*(?:and,?\s*)?\d+\))/;
|
|
30215
|
+
const ENUMERATION_SPLIT = /(?:^|\s*;\s*(?:and,?\s*)?)\d+\)\s*/;
|
|
30216
|
+
/** kasu-ui's stub for an unauthored description; never a clause. */
|
|
30217
|
+
const EMPTY_DESCRIPTION = '—';
|
|
30218
|
+
/**
|
|
30219
|
+
* A risk-item description as the lines beneath the item's name.
|
|
30220
|
+
*
|
|
30221
|
+
* A numbered enumeration (`'1) Foo; and, 2) Bar'`, or one that omits its
|
|
30222
|
+
* leading `1)`: `'Foo; 2) Bar'`) is one line per clause; anything else is
|
|
30223
|
+
* one line. Zero-width characters are stripped first. An unauthored
|
|
30224
|
+
* description is NO lines (`[]`), never a dash.
|
|
30225
|
+
*/
|
|
30226
|
+
function splitNumberedClauses(description) {
|
|
30227
|
+
const raw = cleanDescription(description);
|
|
30228
|
+
if (!raw)
|
|
30229
|
+
return [];
|
|
30230
|
+
const clauses = HAS_ENUMERATION.test(raw)
|
|
30231
|
+
? raw
|
|
30232
|
+
.split(ENUMERATION_SPLIT)
|
|
30233
|
+
.map((part) => part.trim())
|
|
30234
|
+
.filter((part) => part.length > 0)
|
|
30235
|
+
: [raw];
|
|
30236
|
+
const lines = clauses.length > 0 ? clauses : [raw];
|
|
30237
|
+
return lines.filter((line) => line !== EMPTY_DESCRIPTION);
|
|
30238
|
+
}
|
|
30239
|
+
/** A description with zero-width characters stripped, trimmed; `''` when not a string. */
|
|
30240
|
+
function cleanDescription(description) {
|
|
30241
|
+
return typeof description === 'string'
|
|
30242
|
+
? description.replace(ZERO_WIDTH, '').trim()
|
|
30243
|
+
: '';
|
|
30244
|
+
}
|
|
30245
|
+
/**
|
|
30246
|
+
* Risk-management items grouped by their group, the groups in
|
|
30247
|
+
* `RISK_GROUP_ORDER` and, within a group, the items in CMS order.
|
|
30248
|
+
*
|
|
30249
|
+
* An item with no name, or no group, is left out: there is nothing to head
|
|
30250
|
+
* its lines with, or no group to place it in. (The CMS joins items to groups
|
|
30251
|
+
* by name, so neither occurs in live data.) No group is ever empty, so an
|
|
30252
|
+
* item list with nothing usable gives `[]`.
|
|
30253
|
+
*/
|
|
30254
|
+
function groupRiskManagementItems(items) {
|
|
30255
|
+
const groups = new Map();
|
|
30256
|
+
for (const source of items) {
|
|
30257
|
+
const name = trimmedOrUndefined(source.name);
|
|
30258
|
+
const group = trimmedOrUndefined(source.group);
|
|
30259
|
+
if (!name || !group)
|
|
30260
|
+
continue;
|
|
30261
|
+
const clauses = splitNumberedClauses(source.description);
|
|
30262
|
+
// The same cleaned text the clauses come from, and only when they
|
|
30263
|
+
// say something: a description of "—" or of zero-width characters
|
|
30264
|
+
// alone is unauthored, so it is absent here too, never a stray dash.
|
|
30265
|
+
const description = clauses.length > 0 ? cleanDescription(source.description) : '';
|
|
30266
|
+
const tooltip = trimmedOrUndefined(source.tooltip);
|
|
30267
|
+
const item = Object.assign(Object.assign(Object.assign({ name }, (description ? { description } : {})), { clauses }), (tooltip ? { tooltip } : {}));
|
|
30268
|
+
const existing = groups.get(group);
|
|
30269
|
+
if (existing)
|
|
30270
|
+
existing.items.push(item);
|
|
30271
|
+
else
|
|
30272
|
+
groups.set(group, { group, items: [item] });
|
|
30273
|
+
}
|
|
30274
|
+
// Array.prototype.sort is stable, so unlisted groups keep CMS order.
|
|
30275
|
+
return [...groups.values()].sort((a, b) => riskGroupRank(a.group) - riskGroupRank(b.group));
|
|
30276
|
+
}
|
|
30277
|
+
const RISK_GROUP_RANK = new Map(RISK_GROUP_ORDER.map((group, index) => [group.toLowerCase(), index]));
|
|
30278
|
+
function riskGroupRank(group) {
|
|
30279
|
+
var _a;
|
|
30280
|
+
return ((_a = RISK_GROUP_RANK.get(group.trim().toLowerCase())) !== null && _a !== void 0 ? _a : RISK_GROUP_ORDER.length);
|
|
30281
|
+
}
|
|
30282
|
+
/**
|
|
30283
|
+
* The pool's security tags (the CMS `security` list), trimmed, blanks
|
|
30284
|
+
* dropped. `undefined` when none remain.
|
|
30285
|
+
*
|
|
30286
|
+
* kasu-ui shows these under "Security Structure over End Borrowers" ONLY
|
|
30287
|
+
* when the pool has no risk-management items.
|
|
30288
|
+
*/
|
|
30289
|
+
function securityStructureOf(raw) {
|
|
30290
|
+
if (!Array.isArray(raw))
|
|
30291
|
+
return undefined;
|
|
30292
|
+
const tags = raw
|
|
30293
|
+
.map((tag) => trimmedOrUndefined(tag))
|
|
30294
|
+
.filter((tag) => tag !== undefined);
|
|
30295
|
+
return tags.length > 0 ? tags : undefined;
|
|
30296
|
+
}
|
|
30297
|
+
// ---------------------------------------------------------------------------
|
|
30298
|
+
// Numbers
|
|
30299
|
+
// ---------------------------------------------------------------------------
|
|
30300
|
+
/**
|
|
30301
|
+
* A CMS or subgraph "numeric" value as a finite number, or `undefined`. An
|
|
30302
|
+
* empty or blank string is UNAUTHORED, not zero; a genuine `0` survives.
|
|
30303
|
+
*/
|
|
30304
|
+
function finiteOrUndefined(value) {
|
|
30305
|
+
if (typeof value === 'number') {
|
|
30306
|
+
return Number.isFinite(value) ? value : undefined;
|
|
30307
|
+
}
|
|
30308
|
+
if (typeof value === 'string') {
|
|
30309
|
+
if (value.trim() === '')
|
|
30310
|
+
return undefined;
|
|
30311
|
+
const numeric = Number(value);
|
|
30312
|
+
return Number.isFinite(numeric) ? numeric : undefined;
|
|
30313
|
+
}
|
|
30314
|
+
return undefined;
|
|
30315
|
+
}
|
|
30316
|
+
/**
|
|
30317
|
+
* A leg `strategyTotalsUsd` cannot place: two legs for one deployment
|
|
30318
|
+
* (`duplicate-leg`, which would double-count it), or a leg for a deployment
|
|
30319
|
+
* not in `expectedDeployments` (`unexpected-leg`, a pool of some other
|
|
30320
|
+
* strategy). Thrown, because both are caller bugs, not data states.
|
|
30321
|
+
*/
|
|
30322
|
+
class StrategyTotalsInputError extends Error {
|
|
30323
|
+
constructor(code, deployment) {
|
|
30324
|
+
super(code === 'duplicate-leg'
|
|
30325
|
+
? `strategyTotalsUsd: two legs for deployment "${deployment}"`
|
|
30326
|
+
: `strategyTotalsUsd: leg for deployment "${deployment}" is not in expectedDeployments`);
|
|
30327
|
+
this.code = code;
|
|
30328
|
+
this.deployment = deployment;
|
|
30329
|
+
this.name = 'StrategyTotalsInputError';
|
|
30330
|
+
}
|
|
30331
|
+
}
|
|
30332
|
+
/**
|
|
30333
|
+
* Total Active Lending and Lender Interest Earned for a strategy across the
|
|
30334
|
+
* deployments it lends on, in USD: each leg's native amount times that leg's
|
|
30335
|
+
* own `usdRate`, summed. Tax Pay lends on Base (USDC) and XDC (AUDD);
|
|
30336
|
+
* 2,567,901 AUDD is not US$2,567,901.
|
|
30337
|
+
*
|
|
30338
|
+
* `expectedDeployments` is every deployment the strategy has a pool on —
|
|
30339
|
+
* `strategyDeployments(poolId, chain).map((d) => d.chain)` — and `legs` the
|
|
30340
|
+
* ones whose pool the caller actually loaded. What is guaranteed:
|
|
30341
|
+
*
|
|
30342
|
+
* - **`null` — WITHHELD** when there are no legs, or ANY leg's rate is
|
|
30343
|
+
* missing or unusable (not a finite number above 0). The total is never
|
|
30344
|
+
* built on a guessed rate, and never silently drops a loaded leg; the
|
|
30345
|
+
* caller shows the held pool's own figure, or nothing.
|
|
30346
|
+
* - **Otherwise the sum over `legs`**, with `isPartial` /
|
|
30347
|
+
* `missingDeployments` stating exactly which expected deployments it
|
|
30348
|
+
* does NOT cover. `isPartial: false` is the only case in which the
|
|
30349
|
+
* figures are the whole strategy.
|
|
30350
|
+
* - An amount that is absent or malformed contributes nothing (a real `0`
|
|
30351
|
+
* is still `0`).
|
|
30352
|
+
* - Throws `StrategyTotalsInputError` for a duplicated leg or one not in
|
|
30353
|
+
* `expectedDeployments`, before summing anything.
|
|
30354
|
+
*
|
|
30355
|
+
* Only these two figures are summed. The CMS-authored ones — every
|
|
30356
|
+
* originator figure, `operatingSince`, the loss rates — are shared by a
|
|
30357
|
+
* strategy's pools and already strategy-wide: summing them double-counts.
|
|
30358
|
+
*
|
|
30359
|
+
* Mirrors the money half of kasu-ui's `aggregateStrategyPools` and its
|
|
30360
|
+
* withhold gate.
|
|
30361
|
+
*/
|
|
30362
|
+
function strategyTotalsUsd(legs, expectedDeployments) {
|
|
30363
|
+
var _a, _b;
|
|
30364
|
+
const expected = new Set(expectedDeployments);
|
|
30365
|
+
const seen = new Set();
|
|
30366
|
+
for (const leg of legs) {
|
|
30367
|
+
if (seen.has(leg.deployment)) {
|
|
30368
|
+
throw new StrategyTotalsInputError('duplicate-leg', leg.deployment);
|
|
30369
|
+
}
|
|
30370
|
+
if (!expected.has(leg.deployment)) {
|
|
30371
|
+
throw new StrategyTotalsInputError('unexpected-leg', leg.deployment);
|
|
30372
|
+
}
|
|
30373
|
+
seen.add(leg.deployment);
|
|
30374
|
+
}
|
|
30375
|
+
if (legs.length === 0)
|
|
30376
|
+
return null;
|
|
30377
|
+
let totalActiveLendingUsd = 0;
|
|
30378
|
+
let lenderInterestEarnedUsd = 0;
|
|
30379
|
+
for (const leg of legs) {
|
|
30380
|
+
const rate = leg.usdRate;
|
|
30381
|
+
if (typeof rate !== 'number' || !Number.isFinite(rate) || rate <= 0) {
|
|
30382
|
+
return null;
|
|
30383
|
+
}
|
|
30384
|
+
totalActiveLendingUsd +=
|
|
30385
|
+
((_a = finiteOrUndefined(leg.totalActiveLending)) !== null && _a !== void 0 ? _a : 0) * rate;
|
|
30386
|
+
lenderInterestEarnedUsd +=
|
|
30387
|
+
((_b = finiteOrUndefined(leg.lenderInterestEarned)) !== null && _b !== void 0 ? _b : 0) * rate;
|
|
30388
|
+
}
|
|
30389
|
+
const missingDeployments = [...expected].filter((d) => !seen.has(d));
|
|
30390
|
+
return {
|
|
30391
|
+
totalActiveLendingUsd,
|
|
30392
|
+
lenderInterestEarnedUsd,
|
|
30393
|
+
isPartial: missingDeployments.length > 0,
|
|
30394
|
+
missingDeployments,
|
|
30395
|
+
};
|
|
30396
|
+
}
|
|
30397
|
+
function trimmedOrUndefined(value) {
|
|
30398
|
+
if (typeof value !== 'string')
|
|
30399
|
+
return undefined;
|
|
30400
|
+
const trimmed = value.trim();
|
|
30401
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
30402
|
+
}
|
|
30403
|
+
|
|
29312
30404
|
/**
|
|
29313
30405
|
* A strategy's SHORT blurb, as plain text: the CMS `liteDescription` first,
|
|
29314
30406
|
* else the first paragraph of the long `description`, else nothing.
|
|
@@ -29396,10 +30488,55 @@ class StrategiesFacade {
|
|
|
29396
30488
|
* Chain pool id → the pool id its CMS record is keyed by (XDC pools
|
|
29397
30489
|
* share their Base counterpart's editorial record). Lowercase keys.
|
|
29398
30490
|
*/
|
|
29399
|
-
_poolMetadataMapping = {}
|
|
30491
|
+
_poolMetadataMapping = {},
|
|
30492
|
+
/**
|
|
30493
|
+
* The clock the CMS start dates are checked against (a date after it
|
|
30494
|
+
* is refused). Injected for tests; the current time otherwise.
|
|
30495
|
+
*/
|
|
30496
|
+
_now = () => new Date(),
|
|
30497
|
+
/**
|
|
30498
|
+
* Milliseconds on a clock that only moves forward, for the shared
|
|
30499
|
+
* CMS reads' 30-second lifetime — separate from `_now`, so a pinned
|
|
30500
|
+
* date clock cannot keep a read alive for ever. Injected for tests;
|
|
30501
|
+
* `Date.now` otherwise.
|
|
30502
|
+
*/
|
|
30503
|
+
_memoClockMs = () => Date.now()) {
|
|
29400
30504
|
this._dataService = _dataService;
|
|
29401
30505
|
this._userLending = _userLending;
|
|
29402
30506
|
this._poolMetadataMapping = _poolMetadataMapping;
|
|
30507
|
+
this._now = _now;
|
|
30508
|
+
this._memoClockMs = _memoClockMs;
|
|
30509
|
+
/**
|
|
30510
|
+
* Short-lived, per-instance memo of the two CMS-wide reads the pool
|
|
30511
|
+
* getters share, so a Details screen asking for the loss rate, the risk
|
|
30512
|
+
* groups and the originator together reads each collection once. Keyed
|
|
30513
|
+
* by read; holds the in-flight promise, so concurrent callers share it.
|
|
30514
|
+
* A REJECTED read is dropped at once, never served again — the next call
|
|
30515
|
+
* retries.
|
|
30516
|
+
*/
|
|
30517
|
+
this._cmsReads = new Map();
|
|
30518
|
+
}
|
|
30519
|
+
_memoCmsRead(key, read) {
|
|
30520
|
+
const nowMs = this._memoClockMs();
|
|
30521
|
+
const cached = this._cmsReads.get(key);
|
|
30522
|
+
if (cached && cached.expiresAt > nowMs) {
|
|
30523
|
+
return cached.promise;
|
|
30524
|
+
}
|
|
30525
|
+
const promise = read();
|
|
30526
|
+
const entry = { promise, expiresAt: nowMs + CMS_READ_TTL_MS };
|
|
30527
|
+
this._cmsReads.set(key, entry);
|
|
30528
|
+
promise.catch(() => {
|
|
30529
|
+
// Only our own entry: a newer read may have replaced it.
|
|
30530
|
+
if (this._cmsReads.get(key) === entry)
|
|
30531
|
+
this._cmsReads.delete(key);
|
|
30532
|
+
});
|
|
30533
|
+
return promise;
|
|
30534
|
+
}
|
|
30535
|
+
_riskManagementRecords() {
|
|
30536
|
+
return this._memoCmsRead('riskManagement', () => this._dataService.getRiskManagement());
|
|
30537
|
+
}
|
|
30538
|
+
_originatorProfiles() {
|
|
30539
|
+
return this._memoCmsRead('originatorProfiles', () => this._dataService.getPoolDelegateProfileAndHistory());
|
|
29403
30540
|
}
|
|
29404
30541
|
/**
|
|
29405
30542
|
* The pool's loss rate and first-loss capital, from its CMS
|
|
@@ -29412,12 +30549,14 @@ class StrategiesFacade {
|
|
|
29412
30549
|
* 0..1 fraction.
|
|
29413
30550
|
*
|
|
29414
30551
|
* Requires `directusUrl`; without it the call rejects with
|
|
29415
|
-
* `NO_DIRECTUS_URL_MESSAGE`, like every CMS-only read.
|
|
30552
|
+
* `NO_DIRECTUS_URL_MESSAGE`, like every CMS-only read. Shares its CMS
|
|
30553
|
+
* read with `getRiskManagementGroups` for 30 seconds per instance; a
|
|
30554
|
+
* failed read is never reused.
|
|
29416
30555
|
*/
|
|
29417
30556
|
getRiskPerformance(poolId) {
|
|
29418
30557
|
return __awaiter(this, void 0, void 0, function* () {
|
|
29419
30558
|
const metadataId = this._metadataIdOf(poolId);
|
|
29420
|
-
const records = yield this.
|
|
30559
|
+
const records = yield this._riskManagementRecords();
|
|
29421
30560
|
const record = records.find((r) => String(r.poolIdFK).toLowerCase() === metadataId);
|
|
29422
30561
|
if (!record)
|
|
29423
30562
|
return null;
|
|
@@ -29441,16 +30580,95 @@ class StrategiesFacade {
|
|
|
29441
30580
|
* deployment every profile lists no pools and this resolves `null` —
|
|
29442
30581
|
* the same answer kasu-ui's own lookup gives there today. The metadata
|
|
29443
30582
|
* candidate becomes live once the service resolves the mapping.
|
|
30583
|
+
*
|
|
30584
|
+
* Shares its reads with `getOriginatorHistory` for 30 seconds per
|
|
30585
|
+
* instance; a failed read is never reused. Each call returns its own
|
|
30586
|
+
* copy of the profile, so mutating it changes nothing shared.
|
|
29444
30587
|
*/
|
|
29445
30588
|
getOriginator(poolId) {
|
|
29446
30589
|
return __awaiter(this, void 0, void 0, function* () {
|
|
29447
|
-
var _a;
|
|
29448
30590
|
const candidates = new Set([
|
|
29449
30591
|
poolId.toLowerCase(),
|
|
29450
30592
|
this._metadataIdOf(poolId),
|
|
29451
30593
|
]);
|
|
29452
|
-
const profiles = yield this.
|
|
29453
|
-
|
|
30594
|
+
const profiles = yield this._originatorProfiles();
|
|
30595
|
+
const profile = profiles.find((candidate) => candidate.otherKASUPools.some((pool) => candidates.has(String(pool.id).toLowerCase())));
|
|
30596
|
+
// A copy: the profiles are shared between calls (see
|
|
30597
|
+
// `_memoCmsRead`), so a caller sorting or editing its result must
|
|
30598
|
+
// not change what the next call returns.
|
|
30599
|
+
return profile
|
|
30600
|
+
? Object.assign(Object.assign({}, profile), { otherKASUPools: profile.otherKASUPools.map((pool) => (Object.assign({}, pool))) }) : null;
|
|
30601
|
+
});
|
|
30602
|
+
}
|
|
30603
|
+
/**
|
|
30604
|
+
* The credit originator's ENTIRE lending history for a pool — since when,
|
|
30605
|
+
* total originated, loans outstanding, loss rate — read from the profile
|
|
30606
|
+
* `getOriginator` finds, each figure validated. `null` when no profile
|
|
30607
|
+
* lists the pool, or the profile authors none of the four.
|
|
30608
|
+
*
|
|
30609
|
+
* kasu-ui's Details tab shows these as its Credit Originator strip, and
|
|
30610
|
+
* hides the strip when there is no profile. The money figures are
|
|
30611
|
+
* editorial USD on every deployment: never FX-convert them. An XDC pool
|
|
30612
|
+
* resolves `null` today, as it does in kasu-ui (see `getOriginator`).
|
|
30613
|
+
*
|
|
30614
|
+
* Requires `directusUrl`, like every CMS-only read; rejects when the
|
|
30615
|
+
* read fails. Shares its reads with `getOriginator` for 30 seconds per
|
|
30616
|
+
* instance; a failed read is never reused.
|
|
30617
|
+
*/
|
|
30618
|
+
getOriginatorHistory(poolId) {
|
|
30619
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
30620
|
+
const profile = yield this.getOriginator(poolId);
|
|
30621
|
+
if (!profile)
|
|
30622
|
+
return null;
|
|
30623
|
+
const history = {};
|
|
30624
|
+
const since = readCmsStartDate(profile.delegateLendingHistory, this._now(), { unixSeconds: true });
|
|
30625
|
+
if (since !== undefined)
|
|
30626
|
+
history.since = since;
|
|
30627
|
+
const totalOriginated = finiteOrUndefined(profile.totalLoanFundsOriginated);
|
|
30628
|
+
if (totalOriginated !== undefined) {
|
|
30629
|
+
history.totalOriginated = totalOriginated;
|
|
30630
|
+
}
|
|
30631
|
+
const loansOutstanding = finiteOrUndefined(profile.loansUnderManagement);
|
|
30632
|
+
if (loansOutstanding !== undefined) {
|
|
30633
|
+
history.loansOutstanding = loansOutstanding;
|
|
30634
|
+
}
|
|
30635
|
+
const lossRate = finiteOrUndefined(profile.historicLossRate);
|
|
30636
|
+
if (lossRate !== undefined)
|
|
30637
|
+
history.lossRate = lossRate;
|
|
30638
|
+
return Object.keys(history).length > 0 ? history : null;
|
|
30639
|
+
});
|
|
30640
|
+
}
|
|
30641
|
+
/**
|
|
30642
|
+
* The pool's Risk Management groups, from its CMS risk-management record:
|
|
30643
|
+
* each group with its items (name, the pool's description of it split
|
|
30644
|
+
* into lines, the shared tooltip), the groups in `RISK_GROUP_ORDER`
|
|
30645
|
+
* (Security & Recourse, Structural Controls, Financial Covenants,
|
|
30646
|
+
* Reporting Covenants, Key Eligibility Criteria, then any other in CMS
|
|
30647
|
+
* order). See `groupRiskManagementItems`.
|
|
30648
|
+
*
|
|
30649
|
+
* `null` when the pool has no record, or the record has no usable item —
|
|
30650
|
+
* then kasu-ui falls back to `Strategy.securityStructure`. XDC pools
|
|
30651
|
+
* resolve through the metadata mapping to their Base record, like
|
|
30652
|
+
* `getRiskPerformance`.
|
|
30653
|
+
*
|
|
30654
|
+
* REJECTS when the CMS read fails (an outage, or no `directusUrl` —
|
|
30655
|
+
* `NO_DIRECTUS_URL_MESSAGE`), so a failure stays visible rather than
|
|
30656
|
+
* reading as "no items". For what to SHOW, treat a rejection like `null`:
|
|
30657
|
+
* fall back to `Strategy.securityStructure`, which arrives with the
|
|
30658
|
+
* strategy and needs no second read.
|
|
30659
|
+
*
|
|
30660
|
+
* Shares its CMS read with `getRiskPerformance` for 30 seconds per
|
|
30661
|
+
* instance; a failed read is never reused.
|
|
30662
|
+
*/
|
|
30663
|
+
getRiskManagementGroups(poolId) {
|
|
30664
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
30665
|
+
const metadataId = this._metadataIdOf(poolId);
|
|
30666
|
+
const records = yield this._riskManagementRecords();
|
|
30667
|
+
const record = records.find((r) => String(r.poolIdFK).toLowerCase() === metadataId);
|
|
30668
|
+
if (!record || !Array.isArray(record.items))
|
|
30669
|
+
return null;
|
|
30670
|
+
const groups = groupRiskManagementItems(record.items);
|
|
30671
|
+
return groups.length > 0 ? groups : null;
|
|
29454
30672
|
});
|
|
29455
30673
|
}
|
|
29456
30674
|
_metadataIdOf(poolId) {
|
|
@@ -29624,10 +30842,39 @@ class StrategiesFacade {
|
|
|
29624
30842
|
apyStructure: pool.poolApyStructure,
|
|
29625
30843
|
thumbnailUrl: pool.thumbnailImageUrl,
|
|
29626
30844
|
bannerUrl: pool.bannerImageUrl,
|
|
30845
|
+
strategyHistory: strategyHistoryOf(pool, this._now()),
|
|
30846
|
+
endBorrowers: endBorrowersOf(pool),
|
|
30847
|
+
securityStructure: securityStructureOf(pool.security),
|
|
29627
30848
|
_raw: pool,
|
|
29628
30849
|
};
|
|
29629
30850
|
}
|
|
29630
30851
|
}
|
|
30852
|
+
/**
|
|
30853
|
+
* How long a CMS-wide read is shared between the pool getters. Long enough
|
|
30854
|
+
* for one screen's worth of calls, short enough that a CMS edit shows on the
|
|
30855
|
+
* next visit; apps cache further on their own terms.
|
|
30856
|
+
*/
|
|
30857
|
+
const CMS_READ_TTL_MS = 30000;
|
|
30858
|
+
/** `Strategy.strategyHistory`: an absent figure is an absent key. */
|
|
30859
|
+
function strategyHistoryOf(pool, now) {
|
|
30860
|
+
const history = {};
|
|
30861
|
+
const since = readCmsStartDate(pool.operatingSince, now);
|
|
30862
|
+
if (since !== undefined)
|
|
30863
|
+
history.since = since;
|
|
30864
|
+
const lenderInterestEarned = finiteOrUndefined(pool.yieldEarned);
|
|
30865
|
+
if (lenderInterestEarned !== undefined) {
|
|
30866
|
+
history.lenderInterestEarned = lenderInterestEarned;
|
|
30867
|
+
}
|
|
30868
|
+
return history;
|
|
30869
|
+
}
|
|
30870
|
+
/** Business Types and Facility Type, or `undefined` when neither is authored. */
|
|
30871
|
+
function endBorrowersOf(pool) {
|
|
30872
|
+
const businessTypes = splitBusinessTypes(pool.industryExposure);
|
|
30873
|
+
const facility = parseFacility(pool.loanStructure);
|
|
30874
|
+
if (!businessTypes && !facility)
|
|
30875
|
+
return undefined;
|
|
30876
|
+
return Object.assign(Object.assign({}, (businessTypes ? { businessTypes } : {})), (facility ? { facility } : {}));
|
|
30877
|
+
}
|
|
29631
30878
|
/**
|
|
29632
30879
|
* A CMS text field as a string, or `undefined` when it is absent, not a
|
|
29633
30880
|
* string, or empty (blank included) — the CMS stores an emptied field as `''`, and an empty
|
|
@@ -31167,6 +32414,7 @@ exports.PoolTvlHistoryAbortedError = PoolTvlHistoryAbortedError;
|
|
|
31167
32414
|
exports.PoolTvlHistoryTimeoutError = PoolTvlHistoryTimeoutError;
|
|
31168
32415
|
exports.PortfolioFacade = PortfolioFacade;
|
|
31169
32416
|
exports.READ_ONLY_MESSAGE = READ_ONLY_MESSAGE;
|
|
32417
|
+
exports.RISK_GROUP_ORDER = RISK_GROUP_ORDER;
|
|
31170
32418
|
exports.RIXON_CAPITAL = RIXON_CAPITAL;
|
|
31171
32419
|
exports.SECONDS_PER_WEEK = SECONDS_PER_WEEK;
|
|
31172
32420
|
exports.SPONSORED_CALL_KINDS = SPONSORED_CALL_KINDS;
|
|
@@ -31175,6 +32423,7 @@ exports.SdkConfig = SdkConfig;
|
|
|
31175
32423
|
exports.SponsoredCallInputError = SponsoredCallInputError;
|
|
31176
32424
|
exports.StrategiesFacade = StrategiesFacade;
|
|
31177
32425
|
exports.StrategyDeploymentInputError = StrategyDeploymentInputError;
|
|
32426
|
+
exports.StrategyTotalsInputError = StrategyTotalsInputError;
|
|
31178
32427
|
exports.TvlHistoryInputError = TvlHistoryInputError;
|
|
31179
32428
|
exports.UPPER_MEZZANINE = UPPER_MEZZANINE;
|
|
31180
32429
|
exports.WithdrawFlow = WithdrawFlow;
|
|
@@ -31223,6 +32472,7 @@ exports.getCreditOriginator = getCreditOriginator;
|
|
|
31223
32472
|
exports.getInstitutionalLender = getInstitutionalLender;
|
|
31224
32473
|
exports.getStrategyRegion = getStrategyRegion;
|
|
31225
32474
|
exports.getTrancheDisplayName = getTrancheDisplayName;
|
|
32475
|
+
exports.groupRiskManagementItems = groupRiskManagementItems;
|
|
31226
32476
|
exports.isAuMinimumExempt = isAuMinimumExempt;
|
|
31227
32477
|
exports.isAustralianKyc = isAustralianKyc;
|
|
31228
32478
|
exports.isBelowMinimumCapacity = isBelowMinimumCapacity;
|
|
@@ -31238,6 +32488,7 @@ exports.maxNetRateCeiling = maxNetRateCeiling;
|
|
|
31238
32488
|
exports.netEffectiveApy = netEffectiveApy;
|
|
31239
32489
|
exports.netTrancheApyBounds = netTrancheApyBounds;
|
|
31240
32490
|
exports.nextCycleBoundary = nextCycleBoundary;
|
|
32491
|
+
exports.parseFacility = parseFacility;
|
|
31241
32492
|
exports.parseFormattedMessage = parseFormattedMessage;
|
|
31242
32493
|
exports.parseMinorUnits = parseMinorUnits;
|
|
31243
32494
|
exports.parseTrancheBound = parseTrancheBound;
|
|
@@ -31250,10 +32501,15 @@ exports.poolTvl = poolTvl;
|
|
|
31250
32501
|
exports.projectedAnnualEarnings = projectedAnnualEarnings;
|
|
31251
32502
|
exports.proveDepositInReceipt = proveDepositInReceipt;
|
|
31252
32503
|
exports.proveUserOperationInReceipt = proveUserOperationInReceipt;
|
|
32504
|
+
exports.readCmsStartDate = readCmsStartDate;
|
|
31253
32505
|
exports.resolveBoundShortcuts = resolveBoundShortcuts;
|
|
31254
32506
|
exports.resolveDepositBounds = resolveDepositBounds;
|
|
32507
|
+
exports.securityStructureOf = securityStructureOf;
|
|
31255
32508
|
exports.selectVisiblePools = selectVisiblePools;
|
|
32509
|
+
exports.splitBusinessTypes = splitBusinessTypes;
|
|
32510
|
+
exports.splitNumberedClauses = splitNumberedClauses;
|
|
31256
32511
|
exports.strategyDeployments = strategyDeployments;
|
|
32512
|
+
exports.strategyTotalsUsd = strategyTotalsUsd;
|
|
31257
32513
|
exports.submissionEvents = submissionEvents;
|
|
31258
32514
|
exports.submitBatch = submitBatch;
|
|
31259
32515
|
exports.submitSponsoredCall = submitSponsoredCall;
|