@kasufinance/kasu-sdk 2.15.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.
@@ -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: $epochId }
19040
+ where: { epochId_lt: $recentEpoch }
19034
19041
  first: 1
19035
19042
  ) {
19043
+ epochId
19044
+ shareUpdatesIndex
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
19036
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: $epochId }
19121
+ where: { epochId_lt: $recentEpoch }
19122
+ ) {
19123
+ epochId
19124
+ shareUpdatesIndex
19125
+ shares
19126
+ timestamp
19127
+ }
19128
+ recentTrancheShareUpdates: lendingPoolTrancheShareUpdates(
19129
+ first: $first
19130
+ orderBy: shareUpdatesIndex
19131
+ orderDirection: desc
19132
+ where: { epochId_gte: $recentEpoch }
19080
19133
  ) {
19134
+ epochId
19135
+ shareUpdatesIndex
19081
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 four per-epoch series, one page of each per
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: $toEpoch
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 four series for the window, paginating each by `id` until a short
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) {
@@ -19396,6 +19695,144 @@ function sharesAsOf(updates, epoch) {
19396
19695
  }
19397
19696
  return found;
19398
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`.
19777
+ */
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))
19828
+ continue;
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);
19833
+ }
19834
+ return starts;
19835
+ }
19399
19836
  function groupBy(rows, key) {
19400
19837
  const map = new Map();
19401
19838
  for (const row of rows) {
@@ -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 newest entry equals that figure.
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, _e, _f;
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 (_c = interestByTranche.get(trancheId)) !== null && _c !== void 0 ? _c : []) {
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 userSnapshot = sharesAsOf(userUpdates, epoch);
19442
- const userShares = parseFloat((_d = userSnapshot === null || userSnapshot === void 0 ? void 0 : userSnapshot.shares) !== null && _d !== void 0 ? _d : '0');
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
- locks,
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 [lastEpochData, portfolioUserTrancheBalances, userPoolBalances] = yield Promise.all([
19709
- this._graph.request(lendingPortfolioQuery, {
19710
- userAddress: userAddress.toLowerCase(),
19711
- epochId: parseFloat(currentEpoch),
19712
- lastEpochId: previousEpoch.toNumber(),
19713
- unusedPools: this._kasuConfig.UNUSED_LENDING_POOL_IDS,
19714
- first: YIELD_HISTORY_PAGE_SIZE,
19715
- }),
19716
- this._userLendingService.getPortfolioUserTrancheBalances(userAddress),
19717
- this._userLendingService.getUserPoolBalance(userAddress, poolOverviews.map(({ id }) => id), provider),
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
- ...((_a = mappedUserRequest[ftdId]) !== null && _a !== void 0 ? _a : []),
20255
+ ...((_c = mappedUserRequest[ftdId]) !== null && _c !== void 0 ? _c : []),
19747
20256
  userRequestFtdItem,
19748
20257
  ] }));
19749
20258
  }
19750
20259
  }
19751
- let totalInvestments = ethers.BigNumber.from(0);
19752
- for (const poolOverview of poolOverviews) {
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
- const lendingPoolUserDetails = lastEpochData.user.lendingPoolUserDetails.find(({ lendingPool }) => lendingPool.id === poolOverview.id);
19761
- if (!lendingPoolUserDetails)
19762
- continue;
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 = lendingPoolUserDetails.lendingPoolTrancheUserDetails.find((lendingPool) => lendingPool.tranche.id === tranche.id);
20268
+ const lastEpochUserTrancheDetails = details.lendingPoolTrancheUserDetails.find((lendingPool) => lendingPool.tranche.id === tranche.id);
19766
20269
  const userRequest = userRequestsMap.get(tranche.id);
19767
- const depositDetails = (_b = userRequest === null || userRequest === void 0 ? void 0 : userRequest['0']) !== null && _b !== void 0 ? _b : [];
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
- const lastEpoch = previousEpoch.toNumber();
19779
- const userHasLastEpochShares = Boolean((_c = lastEpochUserTrancheDetails
19780
- .lendingPoolTrancheUserEpochSharesUpdates) === null || _c === void 0 ? void 0 : _c.length);
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
- // The one per-epoch formula `getYieldHistory` runs too, with
19783
- // the same share selection — so the last bar of a history
19784
- // equals these figures.
19785
- const lastEpochYield = epochTrancheYield({
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
- if (portfolioUserTrancheBalance) {
19819
- const portfolioUserTranche = portfolioUserTrancheBalance.find(({ trancheId }) => trancheId === tranche.id);
19820
- if (portfolioUserTranche) {
19821
- userTrancheBalance.balance =
19822
- portfolioUserTranche.userBalance;
19823
- userTrancheBalance.yieldEarned =
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;
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;
19826
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: userPoolBalance.balance.toString(), totalYieldEarningsLifetime: userPoolBalance.yieldEarned.toString(), tranches }));
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
- return Object.assign(Object.assign({}, tranche), { investedAmount: utils$2.formatEther(utils$2.parseEther(trancheBalance.balance).sub(lockedAmount)), yieldEarnings: Object.assign(Object.assign({}, tranche.yieldEarnings), { lifetime: this.precisionToString(parseFloat(totalTrancheYieldEarned) -
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 fetchYieldHistorySeries((variables) => this._graph.request(yieldHistoryPageQuery, variables), { tranches: [...tranches], userTranches, locks }, window);
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