@kasufinance/kasu-sdk 2.15.0 → 2.17.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.
Files changed (42) hide show
  1. package/README.md +17 -1
  2. package/dist/bundle.cjs.js +1144 -221
  3. package/dist/bundle.esm.js +1141 -222
  4. package/dist/domain/index.d.ts +2 -2
  5. package/dist/domain/index.js +1 -1
  6. package/dist/domain/index.js.map +1 -1
  7. package/dist/domain/yield-history.d.ts +25 -1
  8. package/dist/domain/yield-history.js +41 -11
  9. package/dist/domain/yield-history.js.map +1 -1
  10. package/dist/domain/yields.d.ts +200 -15
  11. package/dist/domain/yields.js +211 -14
  12. package/dist/domain/yields.js.map +1 -1
  13. package/dist/services/Locking/locking.d.ts +12 -0
  14. package/dist/services/Locking/locking.js +16 -0
  15. package/dist/services/Locking/locking.js.map +1 -1
  16. package/dist/services/Portfolio/portfolio.d.ts +54 -1
  17. package/dist/services/Portfolio/portfolio.js +357 -80
  18. package/dist/services/Portfolio/portfolio.js.map +1 -1
  19. package/dist/services/Portfolio/queries.d.ts +16 -0
  20. package/dist/services/Portfolio/queries.js +119 -2
  21. package/dist/services/Portfolio/queries.js.map +1 -1
  22. package/dist/services/Portfolio/types.d.ts +94 -6
  23. package/dist/services/Portfolio/yield-history.d.ts +148 -10
  24. package/dist/services/Portfolio/yield-history.js +241 -31
  25. package/dist/services/Portfolio/yield-history.js.map +1 -1
  26. package/dist/services/UserLending/user-lending.d.ts +16 -0
  27. package/dist/services/UserLending/user-lending.js +24 -0
  28. package/dist/services/UserLending/user-lending.js.map +1 -1
  29. package/dist/services/shared.d.ts +7 -0
  30. package/dist/services/shared.js +48 -0
  31. package/dist/services/shared.js.map +1 -1
  32. package/package.json +1 -1
  33. package/src/domain/index.ts +5 -1
  34. package/src/domain/yield-history.ts +47 -11
  35. package/src/domain/yields.ts +257 -15
  36. package/src/services/Locking/locking.ts +17 -0
  37. package/src/services/Portfolio/portfolio.ts +559 -126
  38. package/src/services/Portfolio/queries.ts +119 -2
  39. package/src/services/Portfolio/types.ts +91 -6
  40. package/src/services/Portfolio/yield-history.ts +405 -56
  41. package/src/services/UserLending/user-lending.ts +46 -0
  42. package/src/services/shared.ts +48 -0
@@ -15949,6 +15949,54 @@ function isMissingEntityError(error) {
15949
15949
  function isEmptyHistoryError(error, liteDeployment) {
15950
15950
  return liteDeployment && isMissingEntityError(error);
15951
15951
  }
15952
+ /**
15953
+ * What node implementations and RPC providers answer when they cannot serve
15954
+ * state at a requested block: pruned history, or a block they have not seen
15955
+ * yet. Matched on the error's text, lowercased.
15956
+ */
15957
+ const BLOCK_UNAVAILABLE_PATTERNS = [
15958
+ 'missing trie node',
15959
+ 'header not found',
15960
+ 'unknown block',
15961
+ 'block not found',
15962
+ 'pruned',
15963
+ 'historical state',
15964
+ 'state is not available',
15965
+ 'state not available',
15966
+ 'missing state',
15967
+ 'state unavailable',
15968
+ 'state histories',
15969
+ 'block number out of range',
15970
+ 'beyond current head',
15971
+ 'is in the future',
15972
+ ];
15973
+ function errorTexts(error, depth = 0) {
15974
+ if (depth > 4 || error === null || error === undefined)
15975
+ return [];
15976
+ if (typeof error === 'string')
15977
+ return [error];
15978
+ if (typeof error !== 'object')
15979
+ return [];
15980
+ const record = error;
15981
+ return [
15982
+ 'message',
15983
+ 'reason',
15984
+ 'body',
15985
+ 'error',
15986
+ 'data',
15987
+ 'serverError',
15988
+ ].flatMap((key) => errorTexts(record[key], depth + 1));
15989
+ }
15990
+ /**
15991
+ * True when a failed RPC read says the node cannot serve state at the block
15992
+ * asked for (pruned, or not reached yet) — and ONLY then. A rate limit, a
15993
+ * timeout, a refused connection or a revert is not this, and must surface as
15994
+ * the error it is.
15995
+ */
15996
+ function isBlockUnavailableError(error) {
15997
+ const text = errorTexts(error).join(' ').toLowerCase();
15998
+ return BLOCK_UNAVAILABLE_PATTERNS.some((pattern) => text.includes(pattern));
15999
+ }
15952
16000
 
15953
16001
  /**
15954
16002
  * The error a CMS-only call raises on a deployment configured without
@@ -18042,6 +18090,22 @@ class KSULocking {
18042
18090
  return (yield this._systemVariablesAbi.nextEpochStartTimestamp()).toNumber();
18043
18091
  });
18044
18092
  }
18093
+ /**
18094
+ * Unix SECONDS at which epoch 0 started:
18095
+ * `SystemVariables.epochStartTimestamp(0)`. The contract writes this once,
18096
+ * at initialisation, so it never changes for a deployment and may be cached
18097
+ * for the life of the app. Every epoch boundary follows from it —
18098
+ * `epochEndTimestamp(initial, epoch)` and `epochAccrual` in the domain
18099
+ * layer.
18100
+ *
18101
+ * Available on every deployment, Full and Lite. A failed read rejects; the
18102
+ * caller validates the value (a positive integer) before using it.
18103
+ */
18104
+ getInitialEpochStartTimestamp() {
18105
+ return __awaiter(this, void 0, void 0, function* () {
18106
+ return (yield this._systemVariablesAbi.epochStartTimestamp(0)).toNumber();
18107
+ });
18108
+ }
18045
18109
  getNextClearingPeriodDate() {
18046
18110
  return __awaiter(this, void 0, void 0, function* () {
18047
18111
  const nextEpochDate = yield this.getNextEpochDate();
@@ -18078,101 +18142,6 @@ class KSULocking {
18078
18142
  }
18079
18143
  }
18080
18144
 
18081
- /**
18082
- * Settled interest per cycle (epoch): what a lender was credited at each close,
18083
- * per pool and tranche, and the one formula that turns a tranche's credited
18084
- * interest into one lender's share of it.
18085
- *
18086
- * **The figures are NET, and only net.** `LendingPool._applyTrancheInterest`
18087
- * takes the performance fee off first and emits `InterestApplied` with the
18088
- * lender side only (`interest × (1 − performanceFee)`); a fixed-term top-up
18089
- * (`_applyFixedRateInterests`) is net of the fee the same way. So the figure
18090
- * this module computes from those records IS what the lender was credited —
18091
- * never take the fee off it again. No gross is offered: the subgraph keeps
18092
- * only TODAY's fee, so a historical gross would be a guess.
18093
- *
18094
- * Units: amounts are display units of the DEPLOYMENT'S OWN stable token (USDC
18095
- * on Base and XDC USDC, AUDD on XDC AUDD) — never converted to USD here;
18096
- * shares are display units of tranche shares, as the subgraph's BigDecimals.
18097
- */
18098
- /**
18099
- * The ONE per-epoch formula. `Portfolio.getPortfolioLendingData` runs it for
18100
- * the last settled epoch and `getYieldHistory` for every epoch of a window, so
18101
- * the two figures cannot drift apart.
18102
- *
18103
- * ```
18104
- * base = epochInterest × userShares / trancheShares
18105
- * lockBase = base × lock.sharesBeforeEpoch / userShares (active locks)
18106
- * topUp = (lock.sharesAtEpoch − lock.sharesBeforeEpoch) × trancheBalanceNow / trancheSharesNow
18107
- * variable = base − Σ lockBase
18108
- * total = variable + Σ (lockBase + topUp)
18109
- * ```
18110
- *
18111
- * A zero denominator contributes 0 (a tranche or a lender with no shares
18112
- * earned nothing at that close), never `NaN`.
18113
- */
18114
- function epochTrancheYield(input) {
18115
- const base = input.trancheShares === 0
18116
- ? 0
18117
- : (input.epochInterest * input.userShares) / input.trancheShares;
18118
- let variable = base;
18119
- const locks = input.locks.map((lock) => {
18120
- if (!lock.active)
18121
- return 0;
18122
- const topUp = input.trancheSharesNow === 0
18123
- ? 0
18124
- : ((lock.sharesAtEpoch - lock.sharesBeforeEpoch) *
18125
- input.trancheBalanceNow) /
18126
- input.trancheSharesNow;
18127
- const lockBase = input.userShares === 0
18128
- ? 0
18129
- : (base * lock.sharesBeforeEpoch) / input.userShares;
18130
- variable -= lockBase;
18131
- return lockBase + topUp;
18132
- });
18133
- const total = locks.reduce((sum, lockYield) => sum + lockYield, variable);
18134
- return { total, variable, locks };
18135
- }
18136
- /**
18137
- * Pool/tranche entries → one point per epoch, ascending by `epochId`. With no
18138
- * filter it sums every strategy (the Lend tab's bars); with `poolId` it is one
18139
- * strategy (a strategy page's bars).
18140
- *
18141
- * An epoch appears only if a kept entry has it — there is no zero padding, so
18142
- * an empty input (a new lender) is an empty output.
18143
- *
18144
- * For a fixed-term lock holder the bars can sum to slightly MORE than the
18145
- * lender's lifetime interest (up to about 0.2 %): each top-up is valued at the
18146
- * tranche's CURRENT share price, which has grown since. Show "earned to date"
18147
- * from the lifetime figure, never from this sum.
18148
- */
18149
- function aggregateYieldHistory(entries, filter = {}) {
18150
- var _a, _b;
18151
- const poolId = (_a = filter.poolId) === null || _a === void 0 ? void 0 : _a.toLowerCase();
18152
- const trancheId = (_b = filter.trancheId) === null || _b === void 0 ? void 0 : _b.toLowerCase();
18153
- const byEpoch = new Map();
18154
- for (const entry of entries) {
18155
- if (poolId !== undefined && entry.poolId.toLowerCase() !== poolId) {
18156
- continue;
18157
- }
18158
- if (trancheId !== undefined &&
18159
- entry.trancheId.toLowerCase() !== trancheId) {
18160
- continue;
18161
- }
18162
- const point = byEpoch.get(entry.epochId);
18163
- if (point) {
18164
- point.netYield += entry.netYield;
18165
- continue;
18166
- }
18167
- byEpoch.set(entry.epochId, {
18168
- epochId: entry.epochId,
18169
- epochEnd: entry.epochEnd,
18170
- netYield: entry.netYield,
18171
- });
18172
- }
18173
- return [...byEpoch.values()].sort((a, b) => a.epochId - b.epochId);
18174
- }
18175
-
18176
18145
  /**
18177
18146
  * Check if the SDK is configured for a Lite deployment.
18178
18147
  * Lite deployments don't have KSU token, locking, or loyalty features.
@@ -18850,6 +18819,30 @@ class UserLending {
18850
18819
  });
18851
18820
  });
18852
18821
  }
18822
+ /**
18823
+ * The lender's ACTIVE assets in each tranche, read from the chain
18824
+ * (`LendingPoolTranche.userActiveAssets`), in the order of `trancheIds`,
18825
+ * in the stable token's base units. Active shares are the ones the lender
18826
+ * owns in the tranche — fixed-term-locked and withdrawal-pending shares
18827
+ * included — whatever path brought them there; `LendingPool.userBalance`
18828
+ * is their sum. One call per tranche.
18829
+ */
18830
+ getUserTrancheActiveAssets(user, trancheIds, provider, blockTag) {
18831
+ return __awaiter(this, void 0, void 0, function* () {
18832
+ 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' })));
18833
+ });
18834
+ }
18835
+ /**
18836
+ * The lender's balance in each pool (`LendingPool.userBalance`, the sum
18837
+ * of its tranches' active assets), in the order of `poolIds`, in the
18838
+ * stable token's base units, at `blockTag` (default `latest`). One call
18839
+ * per pool.
18840
+ */
18841
+ getUserPoolBalancesAt(user, poolIds, provider, blockTag) {
18842
+ return __awaiter(this, void 0, void 0, function* () {
18843
+ 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' })));
18844
+ });
18845
+ }
18853
18846
  getPortfolioUserTrancheBalances(user) {
18854
18847
  return __awaiter(this, void 0, void 0, function* () {
18855
18848
  const userDetails = yield this._graph.request(portfolioUserTrancheDetailsQuery, {
@@ -19001,20 +18994,47 @@ gql `
19001
18994
  }
19002
18995
  }
19003
18996
  `;
18997
+ /**
18998
+ * The portfolio read. Its last-epoch figures pair each tranche's newest
18999
+ * interest (epoch E) with the shares in force when clearing E applied it —
19000
+ * the selection `getYieldHistory` makes (`positionEpochYield`). For that it
19001
+ * reads, per lender and tranche, the newest share snapshot tagged before
19002
+ * `$recentEpoch` and EVERY snapshot tagged from it on (with their times), and,
19003
+ * for the listed pools (`$poolIds`) and tranches (`$trancheIds`), the
19004
+ * clearings and share exits of those recent epochs. `$recentEpoch` is six
19005
+ * epochs back (`RECENT_PAIRING_EPOCHS`); a newest interest older than that (a
19006
+ * pool that clears rarely) is priced from a history read of its own. Lock
19007
+ * snapshots are read the same way from one epoch further back
19008
+ * (`$lockRecentEpoch`), since a top-up compares E with E − 1. Every list read
19009
+ * with `$first` must come back SHORT of it — a full one is refused, never
19010
+ * used truncated. `_meta` is the block the answer describes: the chain is
19011
+ * read at that same block.
19012
+ */
19004
19013
  const lendingPortfolioQuery = gql `
19005
19014
  query LastEpochQuery(
19006
19015
  $userAddress: ID!
19007
19016
  $epochId: BigInt!
19008
19017
  $lastEpochId: BigInt!
19018
+ $recentEpoch: BigInt!
19019
+ $lockRecentEpoch: BigInt!
19009
19020
  $unusedPools: [String!]!
19021
+ $poolIds: [String!]!
19022
+ $trancheIds: [String!]!
19010
19023
  $first: Int!
19011
19024
  ) {
19025
+ _meta {
19026
+ block {
19027
+ number
19028
+ }
19029
+ }
19012
19030
  user(id: $userAddress) {
19013
19031
  lendingPoolUserDetails(
19014
19032
  first: $first
19015
19033
  where: { lendingPool_not_in: $unusedPools }
19016
19034
  ) {
19017
19035
  id
19036
+ totalAcceptedDeposits
19037
+ totalAcceptedWithdrawnAmount
19018
19038
  lendingPool {
19019
19039
  id
19020
19040
  name
@@ -19024,16 +19044,34 @@ const lendingPortfolioQuery = gql `
19024
19044
  }
19025
19045
  }
19026
19046
  lendingPoolTrancheUserDetails(first: $first) {
19047
+ id
19027
19048
  shares
19049
+ totalAcceptedDeposits
19050
+ totalAcceptedWithdrawnAmount
19028
19051
  lendingPoolTrancheUserEpochSharesUpdates(
19029
19052
  orderDirection: desc
19030
19053
  orderBy: shareUpdatesIndex
19031
- where: { epochId_lt: $epochId }
19054
+ where: { epochId_lt: $recentEpoch }
19032
19055
  first: 1
19033
19056
  ) {
19057
+ epochId
19058
+ shareUpdatesIndex
19034
19059
  shares
19060
+ timestamp
19061
+ }
19062
+ recentUserShareUpdates: lendingPoolTrancheUserEpochSharesUpdates(
19063
+ orderDirection: desc
19064
+ orderBy: shareUpdatesIndex
19065
+ where: { epochId_gte: $recentEpoch }
19066
+ first: $first
19067
+ ) {
19068
+ epochId
19069
+ shareUpdatesIndex
19070
+ shares
19071
+ timestamp
19035
19072
  }
19036
19073
  userLendingPoolTrancheFixedTermDepositLocks(first: $first) {
19074
+ id
19037
19075
  isLocked
19038
19076
  unlockAmount
19039
19077
  initialAmount
@@ -19065,6 +19103,26 @@ const lendingPortfolioQuery = gql `
19065
19103
  ) {
19066
19104
  shares
19067
19105
  }
19106
+ lockSharesBase: userLendingPoolTrancheFixedTermDepositLockShareUpdate(
19107
+ orderBy: shareUpdatesIndex
19108
+ orderDirection: desc
19109
+ first: 1
19110
+ where: { epochId_lt: $lockRecentEpoch }
19111
+ ) {
19112
+ epochId
19113
+ shareUpdatesIndex
19114
+ shares
19115
+ }
19116
+ lockSharesRecent: userLendingPoolTrancheFixedTermDepositLockShareUpdate(
19117
+ orderBy: shareUpdatesIndex
19118
+ orderDirection: desc
19119
+ first: $first
19120
+ where: { epochId_gte: $lockRecentEpoch }
19121
+ ) {
19122
+ epochId
19123
+ shareUpdatesIndex
19124
+ shares
19125
+ }
19068
19126
  }
19069
19127
  tranche {
19070
19128
  id
@@ -19074,9 +19132,23 @@ const lendingPortfolioQuery = gql `
19074
19132
  first: 1
19075
19133
  orderBy: shareUpdatesIndex
19076
19134
  orderDirection: desc
19077
- where: { epochId_lt: $epochId }
19135
+ where: { epochId_lt: $recentEpoch }
19078
19136
  ) {
19137
+ epochId
19138
+ shareUpdatesIndex
19079
19139
  shares
19140
+ timestamp
19141
+ }
19142
+ recentTrancheShareUpdates: lendingPoolTrancheShareUpdates(
19143
+ first: $first
19144
+ orderBy: shareUpdatesIndex
19145
+ orderDirection: desc
19146
+ where: { epochId_gte: $recentEpoch }
19147
+ ) {
19148
+ epochId
19149
+ shareUpdatesIndex
19150
+ shares
19151
+ timestamp
19080
19152
  }
19081
19153
  lendingPoolTrancheEpochInterest(
19082
19154
  first: 1
@@ -19084,12 +19156,50 @@ const lendingPortfolioQuery = gql `
19084
19156
  orderDirection: desc
19085
19157
  where: { epochId_lt: $epochId }
19086
19158
  ) {
19159
+ epochId
19087
19160
  epochInterestAmount
19088
19161
  }
19089
19162
  }
19090
19163
  }
19091
19164
  }
19092
19165
  }
19166
+ lendingPoolClearings(
19167
+ first: $first
19168
+ orderBy: id
19169
+ orderDirection: asc
19170
+ where: {
19171
+ lendingPool_in: $poolIds
19172
+ epochId_gte: $recentEpoch
19173
+ epochId_lt: $epochId
19174
+ }
19175
+ ) {
19176
+ id
19177
+ epochId
19178
+ startTimestamp
19179
+ lendingPool {
19180
+ id
19181
+ }
19182
+ }
19183
+ recentWithdrawalsAccepted: userRequestEvents(
19184
+ first: $first
19185
+ orderBy: id
19186
+ orderDirection: asc
19187
+ where: {
19188
+ type_in: [WithdrawalAccepted, ImmediateWithdrawalExecuted]
19189
+ tranche_in: $trancheIds
19190
+ epochId_gte: $recentEpoch
19191
+ epochId_lt: $epochId
19192
+ }
19193
+ ) {
19194
+ id
19195
+ type
19196
+ epochId
19197
+ createdOn
19198
+ sharesAmount
19199
+ tranche {
19200
+ id
19201
+ }
19202
+ }
19093
19203
  userRequests(
19094
19204
  first: $first
19095
19205
  where: {
@@ -19117,6 +19227,131 @@ const lendingPortfolioQuery = gql `
19117
19227
  }
19118
19228
  `;
19119
19229
 
19230
+ /**
19231
+ * Settled interest per cycle (epoch): what a lender was credited at each close,
19232
+ * per pool and tranche, and the one formula that turns a tranche's credited
19233
+ * interest into one lender's share of it.
19234
+ *
19235
+ * **The figures are NET, and only net.** `LendingPool._applyTrancheInterest`
19236
+ * takes the performance fee off first and emits `InterestApplied` with the
19237
+ * lender side only (`interest × (1 − performanceFee)`); a fixed-term top-up
19238
+ * (`_applyFixedRateInterests`) is net of the fee the same way. So the figure
19239
+ * this module computes from those records IS what the lender was credited —
19240
+ * never take the fee off it again. No gross is offered: the subgraph keeps
19241
+ * only TODAY's fee, so a historical gross would be a guess.
19242
+ *
19243
+ * Units: amounts are display units of the DEPLOYMENT'S OWN stable token (USDC
19244
+ * on Base and XDC USDC, AUDD on XDC AUDD) — never converted to USD here;
19245
+ * shares are display units of tranche shares, as the subgraph's BigDecimals.
19246
+ */
19247
+ /**
19248
+ * The largest change of a lock's shares, as a fraction of its shares before
19249
+ * the epoch, that one clearing's fixed-term top-up can make. The top-up is
19250
+ * the fixed rate's difference from the variable one on one week's balance
19251
+ * (`LendingPool._applyFixedRateInterests`) — a fraction of a percent at any
19252
+ * real rate — so 5 % is far above every genuine top-up and far below any
19253
+ * accounting fault. A lock-share change outside it is NOT interest: it is
19254
+ * left out of the yield (a share movement is never booked as yield). The one
19255
+ * seen on chain is a lock whose recorded shares were set to zero at a
19256
+ * clearing while its shares stayed in the fixed-term contract.
19257
+ */
19258
+ const MAX_TOP_UP_SHARE_FRACTION = 0.05;
19259
+ /**
19260
+ * True when `sharesBefore → sharesAt` is a change one clearing's fixed-term
19261
+ * top-up can make (`MAX_TOP_UP_SHARE_FRACTION`). A lock with no shares
19262
+ * before has no top-up at all.
19263
+ */
19264
+ function isPlausibleTopUp(sharesBefore, sharesAt) {
19265
+ if (!(sharesBefore > 0))
19266
+ return sharesAt === sharesBefore;
19267
+ return (Math.abs(sharesAt - sharesBefore) <=
19268
+ sharesBefore * MAX_TOP_UP_SHARE_FRACTION);
19269
+ }
19270
+ /**
19271
+ * The ONE per-epoch formula. `Portfolio.getPortfolioLendingData` runs it for
19272
+ * the last settled epoch and `getYieldHistory` for every epoch of a window, so
19273
+ * the two figures cannot drift apart.
19274
+ *
19275
+ * ```
19276
+ * base = epochInterest × userShares / trancheShares
19277
+ * lockBase = base × lock.sharesBeforeEpoch / userShares (active locks)
19278
+ * topUp = (lock.sharesAtEpoch − lock.sharesBeforeEpoch) × trancheBalanceNow / trancheSharesNow
19279
+ * variable = base − Σ lockBase
19280
+ * total = variable + Σ (lockBase + topUp)
19281
+ * ```
19282
+ *
19283
+ * A zero denominator contributes 0 (a tranche or a lender with no shares
19284
+ * earned nothing at that close), never `NaN`. So does a NEGATIVE share count:
19285
+ * the subgraph's per-lender shares are a ledger of accepted deposits and
19286
+ * withdrawals, and a wallet whose shares arrived some other way (moved to it
19287
+ * from another wallet) withdraws more than the ledger ever credited it. That
19288
+ * position is not one the ledger can describe, and its negative count is not
19289
+ * a negative yield. A lock-share change no top-up can make
19290
+ * (`isPlausibleTopUp`) contributes no top-up.
19291
+ */
19292
+ function epochTrancheYield(input) {
19293
+ const base = input.trancheShares > 0 && input.userShares > 0
19294
+ ? (input.epochInterest * input.userShares) / input.trancheShares
19295
+ : 0;
19296
+ let variable = base;
19297
+ const locks = input.locks.map((lock) => {
19298
+ if (!lock.active)
19299
+ return 0;
19300
+ const topUp = input.trancheSharesNow > 0 &&
19301
+ isPlausibleTopUp(lock.sharesBeforeEpoch, lock.sharesAtEpoch)
19302
+ ? ((lock.sharesAtEpoch - lock.sharesBeforeEpoch) *
19303
+ input.trancheBalanceNow) /
19304
+ input.trancheSharesNow
19305
+ : 0;
19306
+ const lockBase = input.userShares > 0
19307
+ ? (base * lock.sharesBeforeEpoch) / input.userShares
19308
+ : 0;
19309
+ variable -= lockBase;
19310
+ return lockBase + topUp;
19311
+ });
19312
+ const total = locks.reduce((sum, lockYield) => sum + lockYield, variable);
19313
+ return { total, variable, locks };
19314
+ }
19315
+ /**
19316
+ * Pool/tranche entries → one point per epoch, ascending by `epochId`. With no
19317
+ * filter it sums every strategy (the Lend tab's bars); with `poolId` it is one
19318
+ * strategy (a strategy page's bars).
19319
+ *
19320
+ * An epoch appears only if a kept entry has it — there is no zero padding, so
19321
+ * an empty input (a new lender) is an empty output.
19322
+ *
19323
+ * For a fixed-term lock holder the bars can sum to slightly MORE than the
19324
+ * lender's lifetime interest (up to about 0.2 %): each top-up is valued at the
19325
+ * tranche's CURRENT share price, which has grown since. Show "earned to date"
19326
+ * from the lifetime figure, never from this sum.
19327
+ */
19328
+ function aggregateYieldHistory(entries, filter = {}) {
19329
+ var _a, _b;
19330
+ const poolId = (_a = filter.poolId) === null || _a === void 0 ? void 0 : _a.toLowerCase();
19331
+ const trancheId = (_b = filter.trancheId) === null || _b === void 0 ? void 0 : _b.toLowerCase();
19332
+ const byEpoch = new Map();
19333
+ for (const entry of entries) {
19334
+ if (poolId !== undefined && entry.poolId.toLowerCase() !== poolId) {
19335
+ continue;
19336
+ }
19337
+ if (trancheId !== undefined &&
19338
+ entry.trancheId.toLowerCase() !== trancheId) {
19339
+ continue;
19340
+ }
19341
+ const point = byEpoch.get(entry.epochId);
19342
+ if (point) {
19343
+ point.netYield += entry.netYield;
19344
+ continue;
19345
+ }
19346
+ byEpoch.set(entry.epochId, {
19347
+ epochId: entry.epochId,
19348
+ epochEnd: entry.epochEnd,
19349
+ netYield: entry.netYield,
19350
+ });
19351
+ }
19352
+ return [...byEpoch.values()].sort((a, b) => a.epochId - b.epochId);
19353
+ }
19354
+
19120
19355
  /** The subgraph returns at most this many rows per collection per request. */
19121
19356
  const YIELD_HISTORY_PAGE_SIZE = 1000;
19122
19357
  /**
@@ -19180,6 +19415,7 @@ const yieldHistoryPositionsQuery = gql `
19180
19415
  }
19181
19416
  lendingPoolTrancheUserDetails(first: $first) {
19182
19417
  id
19418
+ shares
19183
19419
  tranche {
19184
19420
  id
19185
19421
  balance
@@ -19194,32 +19430,50 @@ const yieldHistoryPositionsQuery = gql `
19194
19430
  }
19195
19431
  }
19196
19432
  }
19433
+ _meta {
19434
+ block {
19435
+ number
19436
+ }
19437
+ }
19197
19438
  }
19198
19439
  `;
19199
19440
  /**
19200
- * Round trip 2 onwards: the four per-epoch series, one page of each per
19441
+ * Round trip 2 onwards: the six per-epoch series, one page of each per
19201
19442
  * request, cursor-paginated by `id`. A series that is exhausted is skipped with
19202
19443
  * `@include`, so a history costs as many requests as its LONGEST series has
19203
19444
  * pages — normally one — never one per epoch.
19204
19445
  *
19205
19446
  * Share snapshots are read up to the window's end with no lower bound: the
19206
- * value AS OF the first epoch may come from an update long before it.
19447
+ * value AS OF the first epoch may come from an update long before it. Each
19448
+ * carries its `timestamp`, and the window's clearings their start, because a
19449
+ * cycle's interest is paired with the shares in force when the clearing
19450
+ * APPLIED it (`sharesAtInterest`), which the epoch tag alone gets wrong for a
19451
+ * withdrawal accepted between clearings — so the lender's updates are read one
19452
+ * tag past the window (`$userSharesToEpoch`), where the subgraph puts it.
19453
+ * Every share exit between clearings is read too (`trancheSharesAtInterest`
19454
+ * rebuilds the tranche total from the previous clearing's snapshot).
19207
19455
  */
19208
19456
  const yieldHistoryPageQuery = gql `
19209
19457
  query YieldHistoryPage(
19458
+ $pools: [String!]!
19210
19459
  $tranches: [String!]!
19211
19460
  $userTranches: [String!]!
19212
19461
  $locks: [String!]!
19213
19462
  $fromEpoch: BigInt!
19214
19463
  $toEpoch: BigInt!
19464
+ $userSharesToEpoch: BigInt!
19215
19465
  $interestCursor: ID!
19216
19466
  $trancheSharesCursor: ID!
19217
19467
  $userSharesCursor: ID!
19218
19468
  $lockSharesCursor: ID!
19469
+ $clearingsCursor: ID!
19470
+ $withdrawalsCursor: ID!
19219
19471
  $withInterest: Boolean!
19220
19472
  $withTrancheShares: Boolean!
19221
19473
  $withUserShares: Boolean!
19222
19474
  $withLockShares: Boolean!
19475
+ $withClearings: Boolean!
19476
+ $withWithdrawals: Boolean!
19223
19477
  $first: Int!
19224
19478
  ) {
19225
19479
  interest: lendingPoolTrancheEpochInterests(
@@ -19254,6 +19508,7 @@ const yieldHistoryPageQuery = gql `
19254
19508
  epochId
19255
19509
  shareUpdatesIndex
19256
19510
  shares
19511
+ timestamp
19257
19512
  lendingPoolTranche {
19258
19513
  id
19259
19514
  }
@@ -19265,13 +19520,14 @@ const yieldHistoryPageQuery = gql `
19265
19520
  where: {
19266
19521
  id_gt: $userSharesCursor
19267
19522
  lendingPoolTrancheUserDetails_in: $userTranches
19268
- epochId_lte: $toEpoch
19523
+ epochId_lte: $userSharesToEpoch
19269
19524
  }
19270
19525
  ) @include(if: $withUserShares) {
19271
19526
  id
19272
19527
  epochId
19273
19528
  shareUpdatesIndex
19274
19529
  shares
19530
+ timestamp
19275
19531
  lendingPoolTrancheUserDetails {
19276
19532
  id
19277
19533
  }
@@ -19294,10 +19550,49 @@ const yieldHistoryPageQuery = gql `
19294
19550
  id
19295
19551
  }
19296
19552
  }
19553
+ clearings: lendingPoolClearings(
19554
+ first: $first
19555
+ orderBy: id
19556
+ orderDirection: asc
19557
+ where: {
19558
+ id_gt: $clearingsCursor
19559
+ lendingPool_in: $pools
19560
+ epochId_gte: $fromEpoch
19561
+ epochId_lte: $toEpoch
19562
+ }
19563
+ ) @include(if: $withClearings) {
19564
+ id
19565
+ epochId
19566
+ startTimestamp
19567
+ lendingPool {
19568
+ id
19569
+ }
19570
+ }
19571
+ withdrawals: userRequestEvents(
19572
+ first: $first
19573
+ orderBy: id
19574
+ orderDirection: asc
19575
+ where: {
19576
+ id_gt: $withdrawalsCursor
19577
+ type_in: [WithdrawalAccepted, ImmediateWithdrawalExecuted]
19578
+ tranche_in: $tranches
19579
+ epochId_gte: $fromEpoch
19580
+ epochId_lte: $toEpoch
19581
+ }
19582
+ ) @include(if: $withWithdrawals) {
19583
+ id
19584
+ type
19585
+ epochId
19586
+ createdOn
19587
+ sharesAmount
19588
+ tranche {
19589
+ id
19590
+ }
19591
+ }
19297
19592
  }
19298
19593
  `;
19299
19594
  /**
19300
- * Fetch the four series for the window, paginating each by `id` until a short
19595
+ * Fetch the six series for the window, paginating each by `id` until a short
19301
19596
  * page. Returns the rows and how many requests it took.
19302
19597
  */
19303
19598
  function fetchYieldHistorySeries(request, ids, window) {
@@ -19308,6 +19603,8 @@ function fetchYieldHistorySeries(request, ids, window) {
19308
19603
  trancheShares: [],
19309
19604
  userShares: [],
19310
19605
  lockShares: [],
19606
+ clearings: [],
19607
+ withdrawals: [],
19311
19608
  };
19312
19609
  const series = [
19313
19610
  { key: 'interest', flag: 'withInterest', cursor: 'interestCursor' },
@@ -19326,18 +19623,32 @@ function fetchYieldHistorySeries(request, ids, window) {
19326
19623
  flag: 'withLockShares',
19327
19624
  cursor: 'lockSharesCursor',
19328
19625
  },
19626
+ {
19627
+ key: 'clearings',
19628
+ flag: 'withClearings',
19629
+ cursor: 'clearingsCursor',
19630
+ },
19631
+ {
19632
+ key: 'withdrawals',
19633
+ flag: 'withWithdrawals',
19634
+ cursor: 'withdrawalsCursor',
19635
+ },
19329
19636
  ];
19330
19637
  const cursors = {
19331
19638
  interest: '',
19332
19639
  trancheShares: '',
19333
19640
  userShares: '',
19334
19641
  lockShares: '',
19642
+ clearings: '',
19643
+ withdrawals: '',
19335
19644
  };
19336
19645
  const open = {
19337
19646
  interest: ids.tranches.length > 0,
19338
19647
  trancheShares: ids.tranches.length > 0,
19339
19648
  userShares: ids.userTranches.length > 0,
19340
19649
  lockShares: ids.locks.length > 0,
19650
+ clearings: ids.pools.length > 0,
19651
+ withdrawals: ids.tranches.length > 0,
19341
19652
  };
19342
19653
  let requests = 0;
19343
19654
  while (series.some(({ key }) => open[key])) {
@@ -19345,11 +19656,15 @@ function fetchYieldHistorySeries(request, ids, window) {
19345
19656
  throw new Error(`getYieldHistory: more than ${String(YIELD_HISTORY_MAX_PAGES)} pages for one lender`);
19346
19657
  }
19347
19658
  const variables = {
19659
+ pools: ids.pools,
19348
19660
  tranches: ids.tranches,
19349
19661
  userTranches: ids.userTranches,
19350
19662
  locks: ids.locks,
19351
19663
  fromEpoch: String(window.fromEpoch),
19352
19664
  toEpoch: String(window.toEpoch),
19665
+ // One tag further for the lender: a withdrawal accepted between
19666
+ // clearings is tagged with the epoch AFTER the one it counts in.
19667
+ userSharesToEpoch: String(window.toEpoch + 1),
19353
19668
  first: YIELD_HISTORY_PAGE_SIZE,
19354
19669
  };
19355
19670
  for (const { key, flag, cursor } of series) {
@@ -19394,6 +19709,144 @@ function sharesAsOf(updates, epoch) {
19394
19709
  }
19395
19710
  return found;
19396
19711
  }
19712
+ /**
19713
+ * The snapshot in force when the clearing of `epoch` APPLIED that epoch's
19714
+ * interest — the shares the interest was actually spread over.
19715
+ *
19716
+ * A clearing applies interest FIRST (`ClearingCoordinator.doClearing`: the
19717
+ * `applyInterests` call before steps 1–5), then accepts deposits and
19718
+ * withdrawals. So an update is in force for epoch E's interest when it is
19719
+ * tagged `≤ E` (the subgraph tags every update written during clearing E, or
19720
+ * after it, `E + 1` or later), OR when it was written before clearing E began
19721
+ * (`timestamp < clearingStart`). The second test is the one that matters: a
19722
+ * withdrawal the pool manager accepts BETWEEN clearings
19723
+ * (`forceAcceptWithdrawalRequest`) is tagged `E + 1` by the subgraph, as if a
19724
+ * clearing had accepted it, yet the shares were gone before clearing E.
19725
+ *
19726
+ * Without a clearing start (none recorded) it is `sharesAsOf` — the tag alone.
19727
+ */
19728
+ function sharesAtInterest(updates, epoch, clearingStart) {
19729
+ let found;
19730
+ for (const update of updates) {
19731
+ const inForce = Number(update.epochId) <= epoch ||
19732
+ (clearingStart !== undefined &&
19733
+ Number(update.timestamp) < clearingStart);
19734
+ if (!inForce)
19735
+ continue;
19736
+ if (!found || update.shareUpdatesIndex > found.shareUpdatesIndex) {
19737
+ found = update;
19738
+ }
19739
+ }
19740
+ return found;
19741
+ }
19742
+ /**
19743
+ * The tranche's total shares when the clearing of `epoch` applied its
19744
+ * interest.
19745
+ *
19746
+ * The subgraph writes a tranche snapshot when a clearing ENDS (tagged with
19747
+ * the next epoch) and at each immediate withdrawal, but none when the manager
19748
+ * accepts a withdrawal between clearings — so the newest snapshot can predate
19749
+ * shares that had already left. Instead of guessing the order of events in a
19750
+ * block, the total is rebuilt from the one snapshot whose place is certain:
19751
+ * the FIRST one tagged `epoch`, written when clearing `epoch − 1` ended (the
19752
+ * tag itself starts there). Every share exit between that and clearing
19753
+ * `epoch` is tagged `epoch` (the pool's clearing epoch at the time), and none
19754
+ * can happen during a clearing (`notPendingClearing`,
19755
+ * `verifyClearingNotPending`), while deposits and top-ups only happen inside
19756
+ * one. So: that snapshot less every exit tagged `epoch` before the clearing's
19757
+ * start (the clearing's own acceptances come at or after it).
19758
+ *
19759
+ * Without a clearing start, or without a snapshot tagged `epoch` (a tranche's
19760
+ * first clearing), it is the snapshot in force by tag alone.
19761
+ */
19762
+ function trancheSharesAtInterest(snapshots, exits, epoch, clearingStart) {
19763
+ var _a, _b, _c;
19764
+ let base;
19765
+ if (clearingStart !== undefined) {
19766
+ for (const snapshot of snapshots) {
19767
+ if (Number(snapshot.epochId) !== epoch)
19768
+ continue;
19769
+ if (!base || snapshot.shareUpdatesIndex < base.shareUpdatesIndex) {
19770
+ base = snapshot;
19771
+ }
19772
+ }
19773
+ }
19774
+ if (clearingStart === undefined || !base) {
19775
+ return parseFloat((_b = (_a = sharesAsOf(snapshots, epoch)) === null || _a === void 0 ? void 0 : _a.shares) !== null && _b !== void 0 ? _b : '0');
19776
+ }
19777
+ let shares = parseFloat(base.shares);
19778
+ for (const exit of exits) {
19779
+ if (Number(exit.epochId) === epoch &&
19780
+ Number(exit.createdOn) < clearingStart) {
19781
+ shares -= parseFloat((_c = exit.sharesAmount) !== null && _c !== void 0 ? _c : '0');
19782
+ }
19783
+ }
19784
+ return shares;
19785
+ }
19786
+ /**
19787
+ * THE per-position, per-epoch selection both `getYieldHistory` and
19788
+ * `getPortfolioLendingData` run: the shares in force when clearing `epoch`
19789
+ * applied its interest (`sharesAtInterest`, `trancheSharesAtInterest`), each
19790
+ * lock's snapshots as of `epoch` and `epoch − 1`, then `epochTrancheYield`.
19791
+ */
19792
+ function positionEpochYield(series, epoch, epochInterest, clearingStart) {
19793
+ var _a;
19794
+ const userSnapshot = sharesAtInterest(series.userUpdates, epoch, clearingStart);
19795
+ const userShares = parseFloat((_a = userSnapshot === null || userSnapshot === void 0 ? void 0 : userSnapshot.shares) !== null && _a !== void 0 ? _a : '0');
19796
+ const locks = series.locks.map((lock) => {
19797
+ var _a, _b, _c, _d;
19798
+ return ({
19799
+ active: parseFloat(lock.epochLockStart) < epoch &&
19800
+ parseFloat(lock.epochLockEnd) >= epoch &&
19801
+ userSnapshot !== undefined,
19802
+ // No snapshot yet = the lock's opening shares: a lock made after
19803
+ // clearing E starts at E but its first update is tagged E + 1, and
19804
+ // reading that gap as 0 would book its principal as a top-up.
19805
+ sharesAtEpoch: parseFloat((_b = (_a = sharesAsOf(lock.updates, epoch)) === null || _a === void 0 ? void 0 : _a.shares) !== null && _b !== void 0 ? _b : lock.initialTrancheShares),
19806
+ sharesBeforeEpoch: parseFloat((_d = (_c = sharesAsOf(lock.updates, epoch - 1)) === null || _c === void 0 ? void 0 : _c.shares) !== null && _d !== void 0 ? _d : lock.initialTrancheShares),
19807
+ });
19808
+ });
19809
+ return {
19810
+ yield: epochTrancheYield({
19811
+ epochInterest,
19812
+ userShares,
19813
+ trancheShares: trancheSharesAtInterest(series.trancheUpdates, series.exits, epoch, clearingStart),
19814
+ trancheBalanceNow: series.trancheBalanceNow,
19815
+ trancheSharesNow: series.trancheSharesNow,
19816
+ locks,
19817
+ }),
19818
+ userShares,
19819
+ };
19820
+ }
19821
+ /** A position's `PositionSeries` out of the fetched history series. */
19822
+ function positionSeriesFrom(rows, position) {
19823
+ const trancheId = position.tranche.id;
19824
+ return {
19825
+ trancheBalanceNow: parseFloat(position.tranche.balance),
19826
+ trancheSharesNow: parseFloat(position.tranche.shares),
19827
+ userUpdates: rows.userShares.filter((row) => row.lendingPoolTrancheUserDetails.id === position.id),
19828
+ trancheUpdates: rows.trancheShares.filter((row) => row.lendingPoolTranche.id === trancheId),
19829
+ exits: rows.withdrawals.filter((row) => row.tranche.id === trancheId),
19830
+ locks: position.userLendingPoolTrancheFixedTermDepositLocks.map((lock) => (Object.assign(Object.assign({}, lock), { updates: rows.lockShares.filter((row) => row.userLendingPoolTrancheFixedTermDepositLock.id ===
19831
+ lock.id) }))),
19832
+ };
19833
+ }
19834
+ /** `pool → epoch → clearing start`, from the clearing rows. */
19835
+ function clearingStarts(clearings) {
19836
+ var _a;
19837
+ const starts = new Map();
19838
+ for (const clearing of clearings) {
19839
+ const start = Number(clearing.startTimestamp);
19840
+ // A clearing not yet started carries 0: no start to pair by.
19841
+ if (!(start > 0))
19842
+ continue;
19843
+ const pool = clearing.lendingPool.id.toLowerCase();
19844
+ const byEpoch = (_a = starts.get(pool)) !== null && _a !== void 0 ? _a : new Map();
19845
+ byEpoch.set(Number(clearing.epochId), start);
19846
+ starts.set(pool, byEpoch);
19847
+ }
19848
+ return starts;
19849
+ }
19397
19850
  function groupBy(rows, key) {
19398
19851
  const map = new Map();
19399
19852
  for (const row of rows) {
@@ -19410,12 +19863,13 @@ function groupBy(rows, key) {
19410
19863
  * Pure: the fetched series → one entry per pool, tranche and settled epoch in
19411
19864
  * which the lender held shares (or a lock earned a top-up). Each entry runs the
19412
19865
  * same `epochTrancheYield` as the portfolio's last-epoch figure, with the same
19413
- * share selection, so the newest entry equals that figure.
19866
+ * share selection (`sharesAtInterest`, `trancheSharesAtInterest`), so the
19867
+ * newest entry equals that figure.
19414
19868
  *
19415
19869
  * Sorted by `epochId`, then `poolId`, then `trancheId`.
19416
19870
  */
19417
19871
  function buildYieldHistory(read, window, clock) {
19418
- var _a, _b, _c, _d, _e, _f;
19872
+ var _a, _b, _c, _d;
19419
19873
  const user = read.positions.user;
19420
19874
  if (!user)
19421
19875
  return [];
@@ -19423,45 +19877,36 @@ function buildYieldHistory(read, window, clock) {
19423
19877
  const trancheSharesByTranche = groupBy(read.trancheShares, (row) => row.lendingPoolTranche.id);
19424
19878
  const userSharesByPosition = groupBy(read.userShares, (row) => row.lendingPoolTrancheUserDetails.id);
19425
19879
  const lockSharesByLock = groupBy(read.lockShares, (row) => row.userLendingPoolTrancheFixedTermDepositLock.id);
19880
+ const withdrawalsByTranche = groupBy(read.withdrawals, (row) => row.tranche.id);
19881
+ const starts = clearingStarts(read.clearings);
19426
19882
  const entries = [];
19427
19883
  for (const pool of user.lendingPoolUserDetails) {
19884
+ const poolStarts = starts.get(pool.lendingPool.id.toLowerCase());
19428
19885
  for (const position of pool.lendingPoolTrancheUserDetails) {
19429
19886
  const trancheId = position.tranche.id;
19430
19887
  const trancheUpdates = (_a = trancheSharesByTranche.get(trancheId)) !== null && _a !== void 0 ? _a : [];
19431
19888
  const userUpdates = (_b = userSharesByPosition.get(position.id)) !== null && _b !== void 0 ? _b : [];
19889
+ const trancheWithdrawals = (_c = withdrawalsByTranche.get(trancheId)) !== null && _c !== void 0 ? _c : [];
19432
19890
  const trancheBalanceNow = parseFloat(position.tranche.balance);
19433
19891
  const trancheSharesNow = parseFloat(position.tranche.shares);
19434
- for (const record of (_c = interestByTranche.get(trancheId)) !== null && _c !== void 0 ? _c : []) {
19892
+ for (const record of (_d = interestByTranche.get(trancheId)) !== null && _d !== void 0 ? _d : []) {
19435
19893
  const epoch = Number(record.epochId);
19436
19894
  if (epoch < window.fromEpoch || epoch > window.toEpoch) {
19437
19895
  continue;
19438
19896
  }
19439
- const userSnapshot = sharesAsOf(userUpdates, epoch);
19440
- const userShares = parseFloat((_d = userSnapshot === null || userSnapshot === void 0 ? void 0 : userSnapshot.shares) !== null && _d !== void 0 ? _d : '0');
19441
- const locks = position.userLendingPoolTrancheFixedTermDepositLocks.map((lock) => {
19442
- var _a, _b, _c, _d, _e;
19443
- const lockUpdates = (_a = lockSharesByLock.get(lock.id)) !== null && _a !== void 0 ? _a : [];
19444
- return {
19445
- active: parseFloat(lock.epochLockStart) < epoch &&
19446
- parseFloat(lock.epochLockEnd) >= epoch &&
19447
- userSnapshot !== undefined,
19448
- // No snapshot yet = the lock's opening
19449
- // shares: a lock made after clearing E
19450
- // starts at E but its first update is
19451
- // tagged E + 1, and reading that gap as 0
19452
- // would book its principal as a top-up.
19453
- sharesAtEpoch: parseFloat((_c = (_b = sharesAsOf(lockUpdates, epoch)) === null || _b === void 0 ? void 0 : _b.shares) !== null && _c !== void 0 ? _c : lock.initialTrancheShares),
19454
- sharesBeforeEpoch: parseFloat((_e = (_d = sharesAsOf(lockUpdates, epoch - 1)) === null || _d === void 0 ? void 0 : _d.shares) !== null && _e !== void 0 ? _e : lock.initialTrancheShares),
19455
- };
19456
- });
19457
- const { total } = epochTrancheYield({
19458
- epochInterest: parseFloat(record.epochInterestAmount),
19459
- userShares,
19460
- trancheShares: parseFloat((_f = (_e = sharesAsOf(trancheUpdates, epoch)) === null || _e === void 0 ? void 0 : _e.shares) !== null && _f !== void 0 ? _f : '0'),
19897
+ const clearingStart = poolStarts === null || poolStarts === void 0 ? void 0 : poolStarts.get(epoch);
19898
+ const { yield: epochYield, userShares } = positionEpochYield({
19461
19899
  trancheBalanceNow,
19462
19900
  trancheSharesNow,
19463
- locks,
19464
- });
19901
+ userUpdates,
19902
+ trancheUpdates,
19903
+ exits: trancheWithdrawals,
19904
+ locks: position.userLendingPoolTrancheFixedTermDepositLocks.map((lock) => {
19905
+ var _a;
19906
+ return (Object.assign(Object.assign({}, lock), { updates: (_a = lockSharesByLock.get(lock.id)) !== null && _a !== void 0 ? _a : [] }));
19907
+ }),
19908
+ }, epoch, parseFloat(record.epochInterestAmount), clearingStart);
19909
+ const total = epochYield.total;
19465
19910
  // Nothing held and nothing topped up: not part of this
19466
19911
  // lender's history (a new lender's history is empty).
19467
19912
  if (userShares <= 0 && total === 0)
@@ -19482,6 +19927,57 @@ function buildYieldHistory(read, window, clock) {
19482
19927
  a.trancheId.localeCompare(b.trancheId));
19483
19928
  }
19484
19929
 
19930
+ /**
19931
+ * How many epochs back the portfolio read pairs a tranche's newest interest
19932
+ * with the shares in force at its clearing (`lendingPortfolioQuery`): the last
19933
+ * six settled epochs, so a pool whose clearings run late or lag a few weeks
19934
+ * behind the others (Base has one) is paired too. A pool whose newest
19935
+ * interest is older (pools clear independently; some only when the
19936
+ * originator repays) is priced from a history read of its own, at that
19937
+ * epoch. The extra rows are a few per lender and tranche.
19938
+ */
19939
+ const RECENT_PAIRING_EPOCHS = 6;
19940
+ /**
19941
+ * The ledger (subgraph) and the chain agree on a balance AT THE SAME BLOCK
19942
+ * within a cent, or a millionth of it — the rounding of two share-price
19943
+ * conversions.
19944
+ */
19945
+ const BALANCE_RECONCILE_TOLERANCE = 0.01;
19946
+ const BALANCE_RECONCILE_RELATIVE = 1e-6;
19947
+ function ledgerAgrees(ledger, chain) {
19948
+ return (Math.abs(ledger - chain) <=
19949
+ Math.max(BALANCE_RECONCILE_TOLERANCE, Math.abs(chain) * BALANCE_RECONCILE_RELATIVE));
19950
+ }
19951
+ /**
19952
+ * A list read with `first: $first` that comes back FULL may have been cut:
19953
+ * refuse it rather than price from a truncated read.
19954
+ */
19955
+ function refuseTruncated(data) {
19956
+ var _a, _b;
19957
+ 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; };
19958
+ const lists = [
19959
+ ['lendingPoolClearings', full(data.lendingPoolClearings)],
19960
+ ['recentWithdrawalsAccepted', full(data.recentWithdrawalsAccepted)],
19961
+ ];
19962
+ for (const pool of (_b = (_a = data.user) === null || _a === void 0 ? void 0 : _a.lendingPoolUserDetails) !== null && _b !== void 0 ? _b : []) {
19963
+ for (const position of pool.lendingPoolTrancheUserDetails) {
19964
+ lists.push([
19965
+ 'recentUserShareUpdates',
19966
+ full(position.recentUserShareUpdates),
19967
+ ], [
19968
+ 'recentTrancheShareUpdates',
19969
+ full(position.tranche.recentTrancheShareUpdates),
19970
+ ], ...position.userLendingPoolTrancheFixedTermDepositLocks.map((lock) => [
19971
+ 'lockSharesRecent',
19972
+ full(lock.lockSharesRecent),
19973
+ ]));
19974
+ }
19975
+ }
19976
+ const cut = lists.find(([, isFull]) => isFull);
19977
+ if (cut) {
19978
+ throw new Error(`getPortfolioLendingData: ${cut[0]} reached ${String(YIELD_HISTORY_PAGE_SIZE)} rows; refusing a possibly truncated read`);
19979
+ }
19980
+ }
19485
19981
  class Portfolio {
19486
19982
  constructor(_kasuConfig, signerOrProvider) {
19487
19983
  this._kasuConfig = _kasuConfig;
@@ -19699,23 +20195,52 @@ class Portfolio {
19699
20195
  }
19700
20196
  getPortfolioLendingData(userAddress, poolOverviews, currentEpoch, provider) {
19701
20197
  return __awaiter(this, void 0, void 0, function* () {
19702
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
20198
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r;
19703
20199
  const SECONDS_PER_EPOCH = 604800; // seconds in a week
19704
20200
  const portfolioLendingPoolsPromise = [];
19705
20201
  const previousEpoch = BigNumber.from(currentEpoch).sub(BigNumber.from(1));
19706
- const [lastEpochData, portfolioUserTrancheBalances, userPoolBalances] = yield Promise.all([
19707
- this._graph.request(lendingPortfolioQuery, {
19708
- userAddress: userAddress.toLowerCase(),
19709
- epochId: parseFloat(currentEpoch),
19710
- lastEpochId: previousEpoch.toNumber(),
19711
- unusedPools: this._kasuConfig.UNUSED_LENDING_POOL_IDS,
19712
- first: YIELD_HISTORY_PAGE_SIZE,
19713
- }),
19714
- this._userLendingService.getPortfolioUserTrancheBalances(userAddress),
19715
- this._userLendingService.getUserPoolBalance(userAddress, poolOverviews.map(({ id }) => id), provider),
19716
- ]);
20202
+ const recentEpoch = Math.max(0, previousEpoch.toNumber() - (RECENT_PAIRING_EPOCHS - 1));
20203
+ const unused = new Set(this._kasuConfig.UNUSED_LENDING_POOL_IDS.map((id) => id.toLowerCase()));
20204
+ const listedPools = poolOverviews.filter(({ id }) => !unused.has(id.toLowerCase()));
20205
+ const lastEpochData = yield this._graph.request(lendingPortfolioQuery, {
20206
+ userAddress: userAddress.toLowerCase(),
20207
+ epochId: parseFloat(currentEpoch),
20208
+ lastEpochId: previousEpoch.toNumber(),
20209
+ recentEpoch,
20210
+ lockRecentEpoch: Math.max(0, recentEpoch - 1),
20211
+ unusedPools: this._kasuConfig.UNUSED_LENDING_POOL_IDS,
20212
+ poolIds: listedPools.map(({ id }) => id.toLowerCase()),
20213
+ trancheIds: listedPools.flatMap(({ tranches }) => tranches.map(({ id }) => id.toLowerCase())),
20214
+ first: YIELD_HISTORY_PAGE_SIZE,
20215
+ });
19717
20216
  if (!lastEpochData.user)
19718
20217
  return [];
20218
+ refuseTruncated(lastEpochData);
20219
+ // The pools the lender has a ledger in AND that are listed: the only
20220
+ // ones read on chain, and the only ones reported.
20221
+ const userPools = listedPools.flatMap((overview) => {
20222
+ var _a;
20223
+ const details = (_a = lastEpochData.user) === null || _a === void 0 ? void 0 : _a.lendingPoolUserDetails.find(({ lendingPool }) => lendingPool.id.toLowerCase() === overview.id.toLowerCase());
20224
+ return details ? [{ overview, details }] : [];
20225
+ });
20226
+ const recentStarts = clearingStarts(lastEpochData.lendingPoolClearings);
20227
+ const recentExits = new Map();
20228
+ for (const exit of lastEpochData.recentWithdrawalsAccepted) {
20229
+ const list = (_a = recentExits.get(exit.tranche.id)) !== null && _a !== void 0 ? _a : [];
20230
+ list.push(exit);
20231
+ recentExits.set(exit.tranche.id, list);
20232
+ }
20233
+ const [chain, staleYields] = yield Promise.all([
20234
+ 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)),
20235
+ this.staleLastEpochYields(userPools.flatMap(({ overview, details }) => details.lendingPoolTrancheUserDetails.map((position) => ({
20236
+ poolId: overview.id,
20237
+ position,
20238
+ }))), recentEpoch),
20239
+ ]);
20240
+ const reconciled = yield Promise.all(userPools.map(({ overview, details }, index) => {
20241
+ var _a;
20242
+ return this.reconcilePool(userAddress, overview, details, (_a = chain.values[index]) !== null && _a !== void 0 ? _a : BigNumber.from(0), chain.blockTag, provider);
20243
+ }));
19719
20244
  const userRequestsMap = new Map();
19720
20245
  for (const userRequest of lastEpochData.userRequests) {
19721
20246
  const trancheId = userRequest.tranche.id;
@@ -19741,28 +20266,22 @@ class Portfolio {
19741
20266
  continue;
19742
20267
  }
19743
20268
  userRequestsMap.set(trancheId, Object.assign(Object.assign({}, mappedUserRequest), { [ftdId]: [
19744
- ...((_a = mappedUserRequest[ftdId]) !== null && _a !== void 0 ? _a : []),
20269
+ ...((_c = mappedUserRequest[ftdId]) !== null && _c !== void 0 ? _c : []),
19745
20270
  userRequestFtdItem,
19746
20271
  ] }));
19747
20272
  }
19748
20273
  }
19749
- let totalInvestments = BigNumber.from(0);
19750
- for (const poolOverview of poolOverviews) {
19751
- const userPoolBalance = userPoolBalances.find(({ poolId }) => poolId === poolOverview.id);
19752
- if (!userPoolBalance) {
19753
- continue;
19754
- }
19755
- totalInvestments = userPoolBalance.balance.add(totalInvestments);
20274
+ for (const [poolIndex, { overview: poolOverview, details },] of userPools.entries()) {
20275
+ const pool = reconciled[poolIndex];
19756
20276
  const tranches = [];
19757
20277
  let poolYieldEarnedLastEpoch = 0;
19758
- const lendingPoolUserDetails = lastEpochData.user.lendingPoolUserDetails.find(({ lendingPool }) => lendingPool.id === poolOverview.id);
19759
- if (!lendingPoolUserDetails)
19760
- continue;
19761
- const portfolioUserTrancheBalance = portfolioUserTrancheBalances.get(poolOverview.id);
20278
+ // A ledger crediting shares the wallet does not hold earns
20279
+ // nothing we can vouch for: its last-cycle figures are withheld.
20280
+ const withheld = ((_d = pool.mismatch) === null || _d === void 0 ? void 0 : _d.kind) === 'ledger-exceeds-chain';
19762
20281
  for (const tranche of poolOverview.tranches) {
19763
- const lastEpochUserTrancheDetails = lendingPoolUserDetails.lendingPoolTrancheUserDetails.find((lendingPool) => lendingPool.tranche.id === tranche.id);
20282
+ const lastEpochUserTrancheDetails = details.lendingPoolTrancheUserDetails.find((lendingPool) => lendingPool.tranche.id === tranche.id);
19764
20283
  const userRequest = userRequestsMap.get(tranche.id);
19765
- const depositDetails = (_b = userRequest === null || userRequest === void 0 ? void 0 : userRequest['0']) !== null && _b !== void 0 ? _b : [];
20284
+ const depositDetails = (_e = userRequest === null || userRequest === void 0 ? void 0 : userRequest['0']) !== null && _e !== void 0 ? _e : [];
19766
20285
  if (!lastEpochUserTrancheDetails) {
19767
20286
  tranches.push(Object.assign(Object.assign({}, tranche), { investedAmount: {
19768
20287
  balance: '0',
@@ -19773,55 +20292,61 @@ class Portfolio {
19773
20292
  }, depositDetails, fixedLoans: [] }));
19774
20293
  continue;
19775
20294
  }
19776
- const lastEpoch = previousEpoch.toNumber();
19777
- const userHasLastEpochShares = Boolean((_c = lastEpochUserTrancheDetails
19778
- .lendingPoolTrancheUserEpochSharesUpdates) === null || _c === void 0 ? void 0 : _c.length);
20295
+ // The tranche's newest interest (epoch E), priced with the
20296
+ // shares in force when clearing E applied it — the one
20297
+ // selection `getYieldHistory` makes (`positionEpochYield`).
20298
+ // Inside the recent window the candidates below are complete.
20299
+ // An older E (a pool that clears rarely — some only when the
20300
+ // originator repays) is priced from a history read of its own,
20301
+ // and is 0 for a lender who has left the tranche.
20302
+ const newestInterest = (_f = lastEpochUserTrancheDetails.tranche
20303
+ .lendingPoolTrancheEpochInterest) === null || _f === void 0 ? void 0 : _f[0];
20304
+ const interestEpoch = (newestInterest === null || newestInterest === void 0 ? void 0 : newestInterest.epochId) === undefined
20305
+ ? undefined
20306
+ : Number(newestInterest.epochId);
19779
20307
  const fixedTermDeposits = lastEpochUserTrancheDetails.userLendingPoolTrancheFixedTermDepositLocks;
19780
- // The one per-epoch formula `getYieldHistory` runs too, with
19781
- // the same share selection — so the last bar of a history
19782
- // equals these figures.
19783
- const lastEpochYield = epochTrancheYield({
19784
- epochInterest: parseFloat((_f = (_e = (_d = lastEpochUserTrancheDetails.tranche
19785
- .lendingPoolTrancheEpochInterest) === null || _d === void 0 ? void 0 : _d[0]) === null || _e === void 0 ? void 0 : _e.epochInterestAmount) !== null && _f !== void 0 ? _f : '0'),
19786
- userShares: parseFloat((_j = (_h = (_g = lastEpochUserTrancheDetails
19787
- .lendingPoolTrancheUserEpochSharesUpdates) === null || _g === void 0 ? void 0 : _g[0]) === null || _h === void 0 ? void 0 : _h.shares) !== null && _j !== void 0 ? _j : '0'),
19788
- trancheShares: parseFloat((_m = (_l = (_k = lastEpochUserTrancheDetails.tranche
19789
- .lendingPoolTrancheShareUpdates) === null || _k === void 0 ? void 0 : _k[0]) === null || _l === void 0 ? void 0 : _l.shares) !== null && _m !== void 0 ? _m : '0'),
19790
- trancheBalanceNow: parseFloat(lastEpochUserTrancheDetails.tranche.balance),
19791
- trancheSharesNow: parseFloat(lastEpochUserTrancheDetails.tranche.shares),
19792
- locks: fixedTermDeposits.map((fixedTermDeposit) => {
19793
- var _a, _b, _c, _d, _e, _f;
19794
- return ({
19795
- active: parseFloat(fixedTermDeposit.epochLockStart) <
19796
- lastEpoch &&
19797
- parseFloat(fixedTermDeposit.epochLockEnd) >=
19798
- lastEpoch &&
19799
- userHasLastEpochShares,
19800
- // No snapshot = the lock's opening shares, exactly
19801
- // as `getYieldHistory` reads it: a lock made after
19802
- // clearing E starts at E with its first update
19803
- // tagged E + 1. Never the CURRENT shares, which
19804
- // would carry a top-up from a clearing that ran
19805
- // after the last settled epoch.
19806
- 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),
19807
- sharesBeforeEpoch: parseFloat((_f = (_e = (_d = fixedTermDeposit
19808
- .userLendingPoolTrancheFixedTermDepositLockShareUpdate) === null || _d === void 0 ? void 0 : _d[0]) === null || _e === void 0 ? void 0 : _e.shares) !== null && _f !== void 0 ? _f : fixedTermDeposit.initialTrancheShares),
19809
- });
19810
- }),
19811
- });
19812
- const userTrancheBalance = {
19813
- balance: '0',
19814
- yieldEarned: 0,
20308
+ const noYield = {
20309
+ total: 0,
20310
+ variable: 0,
20311
+ locks: fixedTermDeposits.map(() => 0),
19815
20312
  };
19816
- if (portfolioUserTrancheBalance) {
19817
- const portfolioUserTranche = portfolioUserTrancheBalance.find(({ trancheId }) => trancheId === tranche.id);
19818
- if (portfolioUserTranche) {
19819
- userTrancheBalance.balance =
19820
- portfolioUserTranche.userBalance;
19821
- userTrancheBalance.yieldEarned =
19822
- portfolioUserTranche.yieldEarned;
19823
- }
20313
+ let lastEpochYield = noYield;
20314
+ if (interestEpoch !== undefined &&
20315
+ interestEpoch < recentEpoch &&
20316
+ !withheld) {
20317
+ lastEpochYield =
20318
+ (_g = staleYields.get(lastEpochUserTrancheDetails.id)) !== null && _g !== void 0 ? _g : noYield;
20319
+ }
20320
+ else if (interestEpoch !== undefined && !withheld) {
20321
+ lastEpochYield = positionEpochYield({
20322
+ trancheBalanceNow: parseFloat(lastEpochUserTrancheDetails.tranche.balance),
20323
+ trancheSharesNow: parseFloat(lastEpochUserTrancheDetails.tranche.shares),
20324
+ userUpdates: [
20325
+ ...((_h = lastEpochUserTrancheDetails.lendingPoolTrancheUserEpochSharesUpdates) !== null && _h !== void 0 ? _h : []),
20326
+ ...((_j = lastEpochUserTrancheDetails.recentUserShareUpdates) !== null && _j !== void 0 ? _j : []),
20327
+ ],
20328
+ trancheUpdates: [
20329
+ ...((_k = lastEpochUserTrancheDetails.tranche
20330
+ .lendingPoolTrancheShareUpdates) !== null && _k !== void 0 ? _k : []),
20331
+ ...((_l = lastEpochUserTrancheDetails.tranche
20332
+ .recentTrancheShareUpdates) !== null && _l !== void 0 ? _l : []),
20333
+ ],
20334
+ exits: (_m = recentExits.get(tranche.id)) !== null && _m !== void 0 ? _m : [],
20335
+ locks: fixedTermDeposits.map((lock) => {
20336
+ var _a, _b;
20337
+ return (Object.assign(Object.assign({}, lock), { updates: [
20338
+ ...((_a = lock.lockSharesBase) !== null && _a !== void 0 ? _a : []),
20339
+ ...((_b = lock.lockSharesRecent) !== null && _b !== void 0 ? _b : []),
20340
+ ] }));
20341
+ }),
20342
+ }, interestEpoch, parseFloat((_o = newestInterest === null || newestInterest === void 0 ? void 0 : newestInterest.epochInterestAmount) !== null && _o !== void 0 ? _o : '0'), (_p = recentStarts
20343
+ .get(poolOverview.id.toLowerCase())) === null || _p === void 0 ? void 0 : _p.get(interestEpoch)).yield;
19824
20344
  }
20345
+ const ledger = pool.tranches.get(tranche.id);
20346
+ const userTrancheBalance = {
20347
+ balance: (_q = ledger === null || ledger === void 0 ? void 0 : ledger.balance) !== null && _q !== void 0 ? _q : '0',
20348
+ yieldEarned: (_r = ledger === null || ledger === void 0 ? void 0 : ledger.yieldEarned) !== null && _r !== void 0 ? _r : 0,
20349
+ };
19825
20350
  let totalLifetimeFtdYield = 0;
19826
20351
  const fixedLoans = fixedTermDeposits.map((fixedTermDeposit, lockIndex) => {
19827
20352
  var _a, _b;
@@ -19869,7 +20394,9 @@ class Portfolio {
19869
20394
  }, fixedLoans,
19870
20395
  depositDetails }));
19871
20396
  }
19872
- portfolioLendingPoolsPromise.push(Object.assign(Object.assign({}, poolOverview), { totalYieldEarningsLastEpoch: this.precisionToString(poolYieldEarnedLastEpoch), totalInvestedAmount: userPoolBalance.balance.toString(), totalYieldEarningsLifetime: userPoolBalance.yieldEarned.toString(), tranches }));
20397
+ 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
20398
+ ? { chainPinned: false }
20399
+ : {})), { tranches }));
19873
20400
  }
19874
20401
  const portfolioLendingPools = portfolioLendingPoolsPromise.map((pool) => (Object.assign(Object.assign({}, pool), { tranches: pool.tranches.map((tranche) => {
19875
20402
  const trancheBalance = tranche.investedAmount;
@@ -19880,7 +20407,14 @@ class Portfolio {
19880
20407
  }, BigNumber.from(0));
19881
20408
  const totalTrancheYieldEarned = trancheBalance.yieldEarned.toFixed(6);
19882
20409
  const totalTrancheFtdYieldEarned = parseFloat(tranche.yieldEarnings.lifetime).toFixed(6);
19883
- return Object.assign(Object.assign({}, tranche), { investedAmount: formatEther(parseEther(trancheBalance.balance).sub(lockedAmount)), yieldEarnings: Object.assign(Object.assign({}, tranche.yieldEarnings), { lifetime: this.precisionToString(parseFloat(totalTrancheYieldEarned) -
20410
+ // Never negative: a variable balance below the locked
20411
+ // amount is a ledger the chain does not back (the pool
20412
+ // carries `ledgerMismatch`), not money owed — flagged.
20413
+ const variableBalance = parseEther(trancheBalance.balance).sub(lockedAmount);
20414
+ const floored = variableBalance.isNegative();
20415
+ return Object.assign(Object.assign(Object.assign(Object.assign({}, tranche), { investedAmount: formatEther(floored ? BigNumber.from(0) : variableBalance) }), (floored
20416
+ ? { investedAmountFloored: true }
20417
+ : {})), { yieldEarnings: Object.assign(Object.assign({}, tranche.yieldEarnings), { lifetime: this.precisionToString(parseFloat(totalTrancheYieldEarned) -
19884
20418
  parseFloat(totalTrancheFtdYieldEarned)) }) });
19885
20419
  }) })));
19886
20420
  return portfolioLendingPools.filter((pool) => !parseUnits(pool.totalInvestedAmount).isZero() ||
@@ -19906,7 +20440,12 @@ class Portfolio {
19906
20440
  * Cost: three parallel `SystemVariables` reads and the positions query in
19907
20441
  * one round, then one subgraph request per page of the longest series
19908
20442
  * (1000 rows a page) — normally two subgraph round trips in all, never one
19909
- * per epoch.
20443
+ * per epoch — alongside one `LendingPool.userBalance` call per pool at
20444
+ * the subgraph's block. A pool whose ledger credits more than the wallet
20445
+ * holds at that block (shares moved away outside a withdrawal) is left
20446
+ * out; if the node says it cannot serve that block, `latest` is read
20447
+ * and nothing is. Any other failure of those reads (a rate limit, a
20448
+ * network error) THROWS: the history needs a working RPC.
19910
20449
  *
19911
20450
  * On a LITE deployment, a subgraph that lacks the entities (a validation
19912
20451
  * error naming them) yields `[]`; on a full deployment that error throws,
@@ -19917,6 +20456,7 @@ class Portfolio {
19917
20456
  */
19918
20457
  getYieldHistory(userAddress_1) {
19919
20458
  return __awaiter(this, arguments, void 0, function* (userAddress, options = {}) {
20459
+ var _a;
19920
20460
  validateYieldHistoryOptions(options);
19921
20461
  try {
19922
20462
  const [currentEpoch, epochZeroStart, epochDuration, positions] = yield Promise.all([
@@ -19935,7 +20475,9 @@ class Portfolio {
19935
20475
  const userTranches = [];
19936
20476
  const tranches = new Set();
19937
20477
  const locks = [];
20478
+ const pools = [];
19938
20479
  for (const pool of positions.user.lendingPoolUserDetails) {
20480
+ pools.push(pool.lendingPool.id);
19939
20481
  for (const position of pool.lendingPoolTrancheUserDetails) {
19940
20482
  userTranches.push(position.id);
19941
20483
  tranches.add(position.tranche.id);
@@ -19946,11 +20488,33 @@ class Portfolio {
19946
20488
  }
19947
20489
  if (userTranches.length === 0)
19948
20490
  return [];
19949
- const { rows } = yield fetchYieldHistorySeries((variables) => this._graph.request(yieldHistoryPageQuery, variables), { tranches: [...tranches], userTranches, locks }, window);
20491
+ const [{ rows }, chain] = yield Promise.all([
20492
+ fetchYieldHistorySeries((variables) => this._graph.request(yieldHistoryPageQuery, variables), { pools, tranches: [...tranches], userTranches, locks }, window),
20493
+ this.readChainAt((_a = positions._meta) === null || _a === void 0 ? void 0 : _a.block.number, (blockTag) => this._userLendingService.getUserPoolBalancesAt(userAddress, pools, undefined, blockTag)),
20494
+ ]);
20495
+ // A pool whose ledger credits shares the wallet does not hold at
20496
+ // the same block (shares moved away outside a withdrawal) has no
20497
+ // history we can vouch for: it is left out. Unpinned, nothing is.
20498
+ const decimals = this._kasuConfig.stableAssetDecimals;
20499
+ const withheld = new Set();
20500
+ if (chain.blockTag !== undefined) {
20501
+ positions.user.lendingPoolUserDetails.forEach((pool, index) => {
20502
+ var _a;
20503
+ const ledger = pool.lendingPoolTrancheUserDetails.reduce((sum, position) => {
20504
+ var _a;
20505
+ return sum +
20506
+ parseFloat(this._userLendingService.convertSharesToAssets((_a = position.shares) !== null && _a !== void 0 ? _a : '0', position.tranche.balance, position.tranche.shares));
20507
+ }, 0);
20508
+ const held = parseFloat(formatUnits((_a = chain.values[index]) !== null && _a !== void 0 ? _a : BigNumber.from(0), decimals));
20509
+ if (ledger > held && !ledgerAgrees(ledger, held)) {
20510
+ withheld.add(pool.lendingPool.id.toLowerCase());
20511
+ }
20512
+ });
20513
+ }
19950
20514
  return buildYieldHistory(Object.assign({ positions }, rows), window, {
19951
20515
  epochZeroStart: epochZeroStart.toNumber(),
19952
20516
  epochDuration: epochDuration.toNumber(),
19953
- });
20517
+ }).filter((entry) => !withheld.has(entry.poolId.toLowerCase()));
19954
20518
  }
19955
20519
  catch (error) {
19956
20520
  if (isEmptyHistoryError(error, this._isLiteDeployment))
@@ -19959,6 +20523,164 @@ class Portfolio {
19959
20523
  }
19960
20524
  });
19961
20525
  }
20526
+ /**
20527
+ * Read the chain at the subgraph's block, so the chain and the ledger
20528
+ * describe the same state: after every clearing the subgraph runs a few
20529
+ * blocks behind, and a `latest` read would then disagree with an
20530
+ * honest ledger (interest moved every balance, deposits were accepted)
20531
+ * and book that difference as a ledger fault.
20532
+ *
20533
+ * Only when the node says it cannot serve that block (pruned, or behind
20534
+ * the subgraph: `isBlockUnavailableError`) does it fall back to `latest`,
20535
+ * and it says so (`blockTag: undefined`): unpinned figures are never
20536
+ * compared with the ledger, and `getPortfolioLendingData` marks the pools
20537
+ * `chainPinned: false`. Any other failure — a rate limit, a timeout, a
20538
+ * network error — is thrown, never turned into an unpinned read.
20539
+ */
20540
+ readChainAt(block, read) {
20541
+ return __awaiter(this, void 0, void 0, function* () {
20542
+ if (block !== undefined) {
20543
+ try {
20544
+ return { values: yield read(block), blockTag: block };
20545
+ }
20546
+ catch (error) {
20547
+ if (!isBlockUnavailableError(error))
20548
+ throw error;
20549
+ }
20550
+ }
20551
+ return { values: yield read(undefined), blockTag: undefined };
20552
+ });
20553
+ }
20554
+ /**
20555
+ * One pool's balances: the ledger's (the subgraph counts a lender's
20556
+ * shares as accepted deposits − accepted withdrawals ± top-ups) checked
20557
+ * against the chain's `LendingPool.userBalance` at the SAME block.
20558
+ *
20559
+ * The ledger does not follow shares that move between wallets any other
20560
+ * way (an administrative move of a position is the case seen on chain):
20561
+ * the receiving wallet goes negative as it withdraws, the sending one
20562
+ * keeps shares it no longer holds. Where the two disagree at that block —
20563
+ * or any ledger balance is negative — the tranche balances are read from
20564
+ * the chain at that block (`userActiveAssets`, one call per tranche), the
20565
+ * tranche's ledger `yieldEarned` moves by the same difference, and the
20566
+ * pool carries `ledgerMismatch`. Unpinned (the node could not serve the
20567
+ * block), only a negative ledger balance is corrected. A pool that agrees
20568
+ * — every ordinary lender — costs nothing extra.
20569
+ *
20570
+ * Lifetime figures stay ledger figures: they cannot tell moved principal
20571
+ * from interest.
20572
+ */
20573
+ reconcilePool(userAddress, overview, details, chainBalance, blockTag, provider) {
20574
+ return __awaiter(this, void 0, void 0, function* () {
20575
+ var _a, _b, _c;
20576
+ const decimals = this._kasuConfig.stableAssetDecimals;
20577
+ const chainTotal = parseFloat(formatUnits(chainBalance, decimals));
20578
+ const lifetime = chainTotal -
20579
+ parseFloat(details.totalAcceptedDeposits) +
20580
+ parseFloat(details.totalAcceptedWithdrawnAmount);
20581
+ const tranches = new Map();
20582
+ for (const position of details.lendingPoolTrancheUserDetails) {
20583
+ const balance = this._userLendingService.convertSharesToAssets(position.shares, position.tranche.balance, position.tranche.shares);
20584
+ tranches.set(position.tranche.id, {
20585
+ balance,
20586
+ yieldEarned: parseFloat(balance) -
20587
+ parseFloat(position.totalAcceptedDeposits) +
20588
+ parseFloat(position.totalAcceptedWithdrawnAmount),
20589
+ });
20590
+ }
20591
+ const ledgerTotal = [...tranches.values()].reduce((sum, { balance }) => sum + parseFloat(balance), 0);
20592
+ const negative = [...tranches.values()].some(({ balance }) => parseFloat(balance) < 0);
20593
+ const disagrees = blockTag !== undefined && !ledgerAgrees(ledgerTotal, chainTotal);
20594
+ if (!negative && !disagrees) {
20595
+ return { chainBalance, lifetime, tranches };
20596
+ }
20597
+ const trancheIds = [
20598
+ ...new Set([
20599
+ ...overview.tranches.map(({ id }) => id),
20600
+ ...tranches.keys(),
20601
+ ]),
20602
+ ];
20603
+ const onChain = yield this._userLendingService.getUserTrancheActiveAssets(userAddress, trancheIds, provider, blockTag);
20604
+ let phantom = false;
20605
+ const reconciled = new Map();
20606
+ for (const [index, trancheId] of trancheIds.entries()) {
20607
+ const fromLedger = tranches.get(trancheId);
20608
+ const ledgerBalance = parseFloat((_a = fromLedger === null || fromLedger === void 0 ? void 0 : fromLedger.balance) !== null && _a !== void 0 ? _a : '0');
20609
+ const balance = formatUnits((_b = onChain[index]) !== null && _b !== void 0 ? _b : BigNumber.from(0), decimals);
20610
+ if (blockTag !== undefined &&
20611
+ ledgerBalance > parseFloat(balance) &&
20612
+ !ledgerAgrees(ledgerBalance, parseFloat(balance))) {
20613
+ phantom = true;
20614
+ }
20615
+ reconciled.set(trancheId, {
20616
+ balance,
20617
+ yieldEarned: ((_c = fromLedger === null || fromLedger === void 0 ? void 0 : fromLedger.yieldEarned) !== null && _c !== void 0 ? _c : 0) +
20618
+ parseFloat(balance) -
20619
+ ledgerBalance,
20620
+ });
20621
+ }
20622
+ return {
20623
+ chainBalance,
20624
+ lifetime,
20625
+ tranches: reconciled,
20626
+ mismatch: {
20627
+ kind: phantom ? 'ledger-exceeds-chain' : 'chain-exceeds-ledger',
20628
+ ledgerBalance: String(ledgerTotal),
20629
+ chainBalance: String(chainTotal),
20630
+ },
20631
+ };
20632
+ });
20633
+ }
20634
+ /**
20635
+ * The last-epoch figures of positions whose newest interest is OLDER than
20636
+ * the recent window. Pools clear independently, and some clear only when
20637
+ * the originator repays, so a pool's last cycle can be weeks back: it is
20638
+ * priced at THAT epoch, from a history read of just those positions over
20639
+ * just those epochs, through the same `positionEpochYield` — so it equals
20640
+ * the history's newest bar.
20641
+ *
20642
+ * A lender who has since left the tranche (no ledger shares now) gets no
20643
+ * figure: an old pool's last cycle is not theirs to show. Costs nothing
20644
+ * when no position qualifies.
20645
+ */
20646
+ staleLastEpochYields(positions, recentEpoch) {
20647
+ return __awaiter(this, void 0, void 0, function* () {
20648
+ var _a;
20649
+ const stale = positions.flatMap(({ poolId, position }) => {
20650
+ var _a;
20651
+ const interest = (_a = position.tranche.lendingPoolTrancheEpochInterest) === null || _a === void 0 ? void 0 : _a[0];
20652
+ if ((interest === null || interest === void 0 ? void 0 : interest.epochId) === undefined)
20653
+ return [];
20654
+ if (!(parseFloat(position.shares) > 0))
20655
+ return [];
20656
+ const epoch = Number(interest.epochId);
20657
+ return epoch < recentEpoch
20658
+ ? [{ poolId, position, epoch, interest }]
20659
+ : [];
20660
+ });
20661
+ const yields = new Map();
20662
+ if (stale.length === 0)
20663
+ return yields;
20664
+ const lockId = (poolId, lock) => { var _a; return (_a = lock.id) !== null && _a !== void 0 ? _a : `${poolId.toLowerCase()}-${lock.lockId}`; };
20665
+ const { rows } = yield fetchYieldHistorySeries((variables) => this._graph.request(yieldHistoryPageQuery, variables), {
20666
+ pools: [...new Set(stale.map(({ poolId }) => poolId))].map((id) => id.toLowerCase()),
20667
+ tranches: [
20668
+ ...new Set(stale.map(({ position }) => position.tranche.id)),
20669
+ ],
20670
+ userTranches: stale.map(({ position }) => position.id),
20671
+ locks: stale.flatMap(({ poolId, position }) => position.userLendingPoolTrancheFixedTermDepositLocks.map((lock) => lockId(poolId, lock))),
20672
+ }, {
20673
+ fromEpoch: Math.min(...stale.map(({ epoch }) => epoch)),
20674
+ toEpoch: Math.max(...stale.map(({ epoch }) => epoch)),
20675
+ });
20676
+ const starts = clearingStarts(rows.clearings);
20677
+ for (const { poolId, position, epoch, interest } of stale) {
20678
+ const series = positionSeriesFrom(rows, Object.assign(Object.assign({}, position), { userLendingPoolTrancheFixedTermDepositLocks: position.userLendingPoolTrancheFixedTermDepositLocks.map((lock) => (Object.assign(Object.assign({}, lock), { id: lockId(poolId, lock) }))) }));
20679
+ 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);
20680
+ }
20681
+ return yields;
20682
+ });
20683
+ }
19962
20684
  /**
19963
20685
  * A money figure as a plain decimal string. Below a millionth of a token
19964
20686
  * (the stable asset's own precision) is float noise, never money, so it
@@ -31534,6 +32256,16 @@ function getTrancheDisplayName(trancheName, pool) {
31534
32256
  * window, and the accrual spreads an epoch's interest across it.
31535
32257
  */
31536
32258
  const SECONDS_PER_WEEK = 604800;
32259
+ /**
32260
+ * The most unpaid interest {@link accruedInterestSince} shows for one pool:
32261
+ * this many epochs of its weekly figure (owner ruling 2026-11-07).
32262
+ *
32263
+ * A pool can stop being cleared without being flagged stopped. On Base, one
32264
+ * pool is more than six epochs behind and another more than a hundred.
32265
+ * Uncapped, the figure would show interest that may never be credited. Four
32266
+ * epochs covers any ordinary late clearing.
32267
+ */
32268
+ const MAX_ACCRUAL_EPOCHS = 4;
31537
32269
  function isUsableFeePercent(feePercent) {
31538
32270
  return (feePercent !== null &&
31539
32271
  Number.isFinite(feePercent) &&
@@ -31587,28 +32319,40 @@ function dailyInterest(weekly) {
31587
32319
  return weekly / 7;
31588
32320
  }
31589
32321
  /**
31590
- * Interest accrued since each pool's OWN last cycle close — the moving part
31591
- * of lifetime interest (lifetime = settled on chain + this).
32322
+ * Interest accrued since each pool's OWN last clearing transaction, capped at
32323
+ * one epoch per pool.
31592
32324
  *
31593
32325
  * ```
31594
32326
  * accrued = Σ_pool weekly_pool × clamp(now − clearing_pool, 0, 604 800) / 604 800
31595
32327
  * ```
31596
32328
  *
31597
- * **Per pool, because pools clear independently.** One shared anchor accrues
31598
- * a stale pool's share from a close it never had, and when another pool
31599
- * clears it drops the total by the stale pool's whole weekly share. Anchored
31600
- * per pool, the total is monotonic across any single pool's clearing: that
31601
- * pool swaps its accrued share for a capitalised one, and no other term moves.
31602
- *
31603
- * **The one-epoch cap is load-bearing.** Clearings are not exactly weekly,
31604
- * and `weeklyInterest` is per EPOCH, so extrapolating past seven days would
31605
- * promise interest the close will not credit; a late clearing under-reports
31606
- * instead. The low clamp makes a close ahead of the local clock zero, never
31607
- * negative.
31608
- *
32329
+ * Per pool, because pools clear independently: one shared anchor accrues a
32330
+ * stale pool's share from a close it never had, and when another pool clears
32331
+ * it drops the total by the stale pool's whole weekly share.
32332
+ *
32333
+ * **The one-epoch cap rested on a wrong premise.** It assumed a late
32334
+ * clearing credits only one epoch, so accruing past seven days would promise
32335
+ * interest no close pays. The contracts say otherwise. `ClearingCoordinator`
32336
+ * states that clearing "must be executed once per epoch for each lending
32337
+ * pool" and that "clearing epochs must be executed in order"; `doClearing`
32338
+ * reverts unless the target epoch is the pool's `nextLendingPoolClearingEpoch`,
32339
+ * and it calls `LendingPool.applyInterests(targetEpoch)` on every clearing,
32340
+ * late or not ("if clearing for the epoch is executed only after the clearing
32341
+ * period, only yield will be applied"). Every missed epoch's interest is
32342
+ * therefore credited when the clearings catch up — on Base, one pool cleared
32343
+ * epochs 116 to 119 in the same block, four weeks credited at once.
32344
+ * Capped, the figure freezes as soon as a clearing is a week late; anchored
32345
+ * on the transaction, it also DIPS on a catch-up (elapsed resets to zero
32346
+ * while only one week is credited per clearing).
32347
+ *
32348
+ * The low clamp makes a close ahead of the local clock zero, never negative.
31609
32349
  * A non-finite term contributes nothing; a non-finite `nowSeconds` accrues
31610
- * nothing. Callers fail closed upstream by not offering an accrual for a pool
31611
- * whose weekly interest or anchor is unknown.
32350
+ * nothing.
32351
+ *
32352
+ * @deprecated Use {@link accruedInterestSince} with {@link epochAccrual}:
32353
+ * identical while clearings run on time, and it keeps accruing, up to four
32354
+ * epochs, when they run late. Kept, behaviour-identical, for existing
32355
+ * callers.
31612
32356
  */
31613
32357
  function accruedInterest(accruals, nowSeconds) {
31614
32358
  if (!Number.isFinite(nowSeconds))
@@ -31623,6 +32367,181 @@ function accruedInterest(accruals, nowSeconds) {
31623
32367
  }
31624
32368
  return accrued;
31625
32369
  }
32370
+ /**
32371
+ * Unix SECONDS at which `epoch` ENDS — the start of the next one:
32372
+ *
32373
+ * ```
32374
+ * epochEnd(e) = initialEpochStart + (e + 1) × 604 800
32375
+ * ```
32376
+ *
32377
+ * `SystemVariables.epochStartTimestamp(e)` is `initial + e × EPOCH_DURATION`,
32378
+ * and the initial start is written once, at initialisation, so the end of any
32379
+ * epoch follows from `epochStartTimestamp(0)` with no further read.
32380
+ *
32381
+ * FAILS CLOSED with `NaN`: a non-integer, non-finite or negative epoch, an
32382
+ * initial start that is not a positive integer, or a result past
32383
+ * `Number.MAX_SAFE_INTEGER`.
32384
+ *
32385
+ * @param initialEpochStart unix seconds, `epochStartTimestamp(0)`.
32386
+ * @param epoch the epoch number, 0-based.
32387
+ */
32388
+ function epochEndTimestamp(initialEpochStart, epoch) {
32389
+ if (!Number.isSafeInteger(initialEpochStart) || initialEpochStart <= 0) {
32390
+ return NaN;
32391
+ }
32392
+ if (!Number.isSafeInteger(epoch) || epoch < 0)
32393
+ return NaN;
32394
+ const end = initialEpochStart + (epoch + 1) * SECONDS_PER_WEEK;
32395
+ return Number.isSafeInteger(end) ? end : NaN;
32396
+ }
32397
+ /**
32398
+ * Builds one pool's {@link EpochAccrual} from what the chain and the subgraph
32399
+ * say about its last clearing:
32400
+ *
32401
+ * ```
32402
+ * epochEnd = epochEndTimestamp(initialEpochStart, lastClearedEpoch)
32403
+ * accruesFrom = min(clearingStartedAt, epochEnd)
32404
+ * ```
32405
+ *
32406
+ * - **On time** (the clearing started inside its window, before its epoch
32407
+ * ended): `accruesFrom` is the clearing's start. Clearing applies the
32408
+ * epoch's interest first and only then accepts requests, so today's
32409
+ * balance — which includes a deposit that clearing accepted — is never
32410
+ * charged for time before it: a new lender starts at exactly 0.
32411
+ * - **Late** (it started at or after its epoch end): `accruesFrom` is the
32412
+ * epoch end. A late clearing accepts no requests and credits exactly one
32413
+ * epoch, so the time it did not credit is kept.
32414
+ *
32415
+ * `lastClearedEpoch` is the newest epoch whose clearing has STARTED (the
32416
+ * subgraph has a `LendingPoolClearing` for it), not the newest that has
32417
+ * finished, and `clearingStartedAt` is that clearing's `startTimestamp`.
32418
+ * `ClearingCoordinator.doClearing` mints the epoch's interest in its FIRST
32419
+ * transaction (`applyInterests`), while `nextLendingPoolClearingEpoch` only
32420
+ * advances when the clearing ENDS, so it is the wrong source.
32421
+ *
32422
+ * FAILS CLOSED with `null`: whatever {@link epochEndTimestamp} rejects, a
32423
+ * `clearingStartedAt` that is not a finite positive number, or a
32424
+ * `weeklyInterest` that is not finite or is negative. The caller then offers
32425
+ * no term for that pool.
32426
+ *
32427
+ * @param input.weeklyInterest the pool's net interest for one epoch.
32428
+ * @param input.initialEpochStart unix seconds, `epochStartTimestamp(0)`.
32429
+ * @param input.lastClearedEpoch the newest epoch whose clearing has started.
32430
+ * @param input.clearingStartedAt unix seconds, that clearing's start.
32431
+ */
32432
+ function epochAccrual(input) {
32433
+ const { weeklyInterest: weekly, clearingStartedAt } = input;
32434
+ if (!Number.isFinite(weekly) || weekly < 0)
32435
+ return null;
32436
+ if (!Number.isFinite(clearingStartedAt) || clearingStartedAt <= 0) {
32437
+ return null;
32438
+ }
32439
+ const epochEnd = epochEndTimestamp(input.initialEpochStart, input.lastClearedEpoch);
32440
+ if (Number.isNaN(epochEnd))
32441
+ return null;
32442
+ return {
32443
+ weeklyInterest: weekly,
32444
+ accruesFrom: Math.min(clearingStartedAt, epochEnd),
32445
+ epochEnd,
32446
+ };
32447
+ }
32448
+ /**
32449
+ * Interest accrued and not yet credited — the moving part of lifetime
32450
+ * interest (lifetime = settled on chain + this). Per term, with
32451
+ * `W` = 604 800 s:
32452
+ *
32453
+ * ```
32454
+ * accrued = min( weekly × ( min(max(now − accruesFrom, 0), W)
32455
+ * + max(now − (epochEnd + W), 0) ) / W,
32456
+ * MAX_ACCRUAL_EPOCHS × weekly )
32457
+ * ```
32458
+ *
32459
+ * Read along the clock, for one pool:
32460
+ * 1. from `accruesFrom`, one epoch of interest accrues linearly;
32461
+ * 2. it then HOLDS from `accruesFrom + W` until `epochEnd + W`, the end of
32462
+ * the next epoch — at most 48 h after an on-time clearing (its clearing
32463
+ * window), no time at all after a late one (`accruesFrom == epochEnd`);
32464
+ * 3. then it resumes, up to {@link MAX_ACCRUAL_EPOCHS} (4) epochs of
32465
+ * `weekly` in all, and stops there.
32466
+ *
32467
+ * **Why it keeps accruing past one epoch.** Clearing must run once per
32468
+ * epoch for each pool, in order (`ClearingCoordinator.doClearing` reverts
32469
+ * unless the target epoch is the pool's `nextLendingPoolClearingEpoch`), and
32470
+ * every clearing, on time or late, starts with
32471
+ * `LendingPool.applyInterests(targetEpoch)`. A missed epoch is therefore
32472
+ * credited when the clearings catch up (on Base, one pool cleared epochs 116
32473
+ * to 119 in the same block).
32474
+ *
32475
+ * **Why it stops at four epochs** (owner ruling 2026-11-07). A pool can stop
32476
+ * being cleared without being flagged stopped. On Base today one pool is
32477
+ * more than six epochs behind and another more than a hundred. Past four
32478
+ * epochs the figure would show interest that may never be credited. Four
32479
+ * covers any ordinary late clearing.
32480
+ *
32481
+ * **Why the hold.** Without it an on-time anchor accrues past one epoch
32482
+ * while the next on-time clearing — which credits exactly one — is still to
32483
+ * come, and the total steps DOWN when it lands. With it, at a constant
32484
+ * balance, `settled + accrued` never steps down at a clearing (`t` = the
32485
+ * NEW clearing's start, `E` = the end of the epoch it clears):
32486
+ * - on time after on time: identical to the deprecated `accruedInterest`
32487
+ * at every instant. With `gap` = `t` − the previous clearing's start, it
32488
+ * steps UP by `weekly × (W − gap) / W` when `gap < W` (at most 2/7 of one
32489
+ * weekly, as before), and not at all when `gap ≥ W`;
32490
+ * - late after on time, and late after late: exactly continuous;
32491
+ * - on time after late: steps UP by `weekly × (E − t) / W`, at most 2/7 of
32492
+ * one weekly.
32493
+ * A deposit or top-up accepted at an on-time clearing starts at 0, so it
32494
+ * never dips the total either.
32495
+ *
32496
+ * **At the cap.** All of the above holds for a pool below the cap, where
32497
+ * the cap changes nothing. A pool AT the cap shows `4 × weekly` before a
32498
+ * catch-up clearing; after it, one more `weekly` is settled and the new term
32499
+ * shows `min(elapsed′, 4 × weekly)`, so `after = min(elapsed′ + weekly,
32500
+ * 5 × weekly) ≥ before`. Each clearing steps the total UP by at most one
32501
+ * weekly until the pool is back under the cap. Never down, and never above
32502
+ * settled plus `4 × weekly` per pool.
32503
+ *
32504
+ * **Never negative**, and non-decreasing in `now`. A term is skipped — it
32505
+ * contributes nothing — when `weeklyInterest` is not finite or is negative,
32506
+ * when `accruesFrom` or `epochEnd` is not finite, when `accruesFrom >
32507
+ * epochEnd` (inconsistent: the anchor is never after the epoch end), or when
32508
+ * an elapsed time overflows. A non-finite `nowSeconds` accrues nothing.
32509
+ *
32510
+ * **Per pool, because pools clear independently.** One shared anchor accrues
32511
+ * a stale pool's share from a close it never had.
32512
+ *
32513
+ * **Accepted approximation.** `weekly` is today's balance at today's epoch
32514
+ * rate. Over a multi-week gap the chain compounds weekly and may apply other
32515
+ * epochs' rates, so a long accrual reads slightly LOW. It is a ticking
32516
+ * estimate, not a ledger.
32517
+ *
32518
+ * Callers fail closed upstream: no term for a pool whose weekly interest or
32519
+ * clearing is unknown (`epochAccrual` returns `null`), and none for a pool
32520
+ * that has stopped — it will never clear again.
32521
+ */
32522
+ function accruedInterestSince(accruals, nowSeconds) {
32523
+ if (!Number.isFinite(nowSeconds))
32524
+ return 0;
32525
+ let accrued = 0;
32526
+ for (const { weeklyInterest: weekly, accruesFrom, epochEnd } of accruals) {
32527
+ if (!Number.isFinite(weekly) || weekly < 0)
32528
+ continue;
32529
+ if (!Number.isFinite(accruesFrom) || !Number.isFinite(epochEnd)) {
32530
+ continue;
32531
+ }
32532
+ if (accruesFrom > epochEnd)
32533
+ continue;
32534
+ const sinceAnchor = nowSeconds - accruesFrom;
32535
+ const sinceHoldEnd = nowSeconds - (epochEnd + SECONDS_PER_WEEK);
32536
+ if (!Number.isFinite(sinceAnchor) || !Number.isFinite(sinceHoldEnd)) {
32537
+ continue;
32538
+ }
32539
+ const elapsed = Math.min(Math.max(sinceAnchor, 0), SECONDS_PER_WEEK) +
32540
+ Math.max(sinceHoldEnd, 0);
32541
+ accrued += Math.min((weekly * elapsed) / SECONDS_PER_WEEK, MAX_ACCRUAL_EPOCHS * weekly);
32542
+ }
32543
+ return accrued;
32544
+ }
31626
32545
  /**
31627
32546
  * What `amount` earns in a year at a NET rate — the "≈ $Y a year" line.
31628
32547
  * Feed it `netEffectiveApy(...)`, never a gross APY: the result is a lender's
@@ -31665,4 +32584,4 @@ function weightedAverageApy(slices) {
31665
32584
  return weighted / totalBalance;
31666
32585
  }
31667
32586
 
31668
- export { APXIUM, AU_ALPHA3, AU_MIN_CUMULATIVE_BY_STABLE, BatchPendingError, BatchRevertedError, BatchSubmissionError, CHAIN_CONFIGS, CLEARING_WINDOW_SECONDS, CONTRACT_TTL_MS, CallsFacade, DAYS_PER_EPOCH, DAYS_PER_MONTH, DEFAULT_BATCH_POLLING_POLICY, DepositBatchMismatchError, DepositFlow, DepositsFacade, ENTRY_POINT_V07, EPOCHS_IN_YEAR, Flow, FlowStore, FlowsFacade, INVALID_ACCEPTANCE_SIGNATURE_MESSAGE, INVALID_AUTH_SIGNATURE_MESSAGE, INVOICEMATE, Kasu, KasuSdk, MAX_LENDING_AMOUNT_FALLBACK, MIN_DAYS_FOR_MONTHS, MIN_LENDING_AMOUNT_FALLBACK, MIN_TRANCHE_CAPACITY, NOT_A_WITHDRAWAL_CALL_MESSAGE, NO_DIRECTUS_URL_MESSAGE, NO_HISTORY_PROVIDER_MESSAGE, NO_KYC_PARAMS_MESSAGE, NO_SPENDER_MESSAGE, NO_WITHDRAWAL_ENCODER_MESSAGE, PastStateUnavailableError, PoolTvlHistoryAbortedError, PoolTvlHistoryTimeoutError, PortfolioFacade, READ_ONLY_MESSAGE, RISK_GROUP_ORDER, RIXON_CAPITAL, SECONDS_PER_WEEK, SPONSORED_CALL_KINDS, SPONSORED_CALL_SELECTORS, SdkConfig, SponsoredCallInputError, StrategiesFacade, StrategyDeploymentInputError, StrategyTotalsInputError, TvlHistoryInputError, UPPER_MEZZANINE, WithdrawFlow, accruedInterest, aggregateYieldHistory, apyToEpochRate, asContractType, auMinimumRemaining, auThresholdFor, availableFixedTermConfigs, batchOutcomeOf, buildContractVersionType, buildFullNameRequestMessage, buildLegacyContractRequestMessage, buildLoanAgreementSignMessage, cancelDeadline, ceilToCents, checkBatchOutcome, checkSponsoredCallOutcome, classifyWalletFailure, combineTvlHistories, compareTrancheSeniority, computeSettlementWindow, countSubmissions, dailyInterest, decodeRevert, deriveCycleDates, derivePoolStatus, deriveRequestState, encodeCancelDepositCall, encodeCancelWithdrawalCall, encodeDepositData, encodeRequestWithdrawalCall, encodeStableTransfer, encodeWithdrawAtExpiryCall, epochRateToApy, epochTrancheYield, explorerAddressUrl, explorerTxUrl, extractRevertData, fetchUnusedPoolIds, firstSubmissionTimestamp, floorToCents, formatSignTimestampUtc, getCreditOriginator, getInstitutionalLender, getStrategyRegion, getTrancheDisplayName, groupRiskManagementItems, isAuMinimumExempt, isAustralianKyc, isBelowMinimumCapacity, isCycleClosed, isDefinitivelyNotSubmitted, isFixedTermConfigId, isUnpredictableGas, isUserRejected, lastEventTimestamp, lockDurationDays, lockDurationMonths, maxNetRateCeiling, netEffectiveApy, netTrancheApyBounds, nextCycleBoundary, parseFacility, parseFormattedMessage, parseMinorUnits, parseTrancheBound, pickDefaultTrancheId, pickHighestYieldTranche, pickShortDescription, poolAllTranchesFull, poolMaxApy, poolTvl, projectedAnnualEarnings, proveDepositInReceipt, proveUserOperationInReceipt, readCmsStartDate, resolveBoundShortcuts, resolveDepositBounds, securityStructureOf, selectVisiblePools, splitBusinessTypes, splitNumberedClauses, strategyDeployments, strategyTotalsUsd, submissionEvents, submitBatch, submitSponsoredCall, trancheApyBounds, trancheHasCapacity, trancheRiskRank, validateDepositBatchCalls, weeklyInterest, weightedAverageApy };
32587
+ export { APXIUM, AU_ALPHA3, AU_MIN_CUMULATIVE_BY_STABLE, BatchPendingError, BatchRevertedError, BatchSubmissionError, CHAIN_CONFIGS, CLEARING_WINDOW_SECONDS, CONTRACT_TTL_MS, CallsFacade, DAYS_PER_EPOCH, DAYS_PER_MONTH, DEFAULT_BATCH_POLLING_POLICY, DepositBatchMismatchError, DepositFlow, DepositsFacade, ENTRY_POINT_V07, EPOCHS_IN_YEAR, Flow, FlowStore, FlowsFacade, INVALID_ACCEPTANCE_SIGNATURE_MESSAGE, INVALID_AUTH_SIGNATURE_MESSAGE, INVOICEMATE, Kasu, KasuSdk, MAX_ACCRUAL_EPOCHS, MAX_LENDING_AMOUNT_FALLBACK, MIN_DAYS_FOR_MONTHS, MIN_LENDING_AMOUNT_FALLBACK, MIN_TRANCHE_CAPACITY, NOT_A_WITHDRAWAL_CALL_MESSAGE, NO_DIRECTUS_URL_MESSAGE, NO_HISTORY_PROVIDER_MESSAGE, NO_KYC_PARAMS_MESSAGE, NO_SPENDER_MESSAGE, NO_WITHDRAWAL_ENCODER_MESSAGE, PastStateUnavailableError, PoolTvlHistoryAbortedError, PoolTvlHistoryTimeoutError, PortfolioFacade, READ_ONLY_MESSAGE, RISK_GROUP_ORDER, RIXON_CAPITAL, SECONDS_PER_WEEK, SPONSORED_CALL_KINDS, SPONSORED_CALL_SELECTORS, SdkConfig, SponsoredCallInputError, StrategiesFacade, StrategyDeploymentInputError, StrategyTotalsInputError, TvlHistoryInputError, UPPER_MEZZANINE, WithdrawFlow, accruedInterest, accruedInterestSince, aggregateYieldHistory, apyToEpochRate, asContractType, auMinimumRemaining, auThresholdFor, availableFixedTermConfigs, batchOutcomeOf, buildContractVersionType, buildFullNameRequestMessage, buildLegacyContractRequestMessage, buildLoanAgreementSignMessage, cancelDeadline, ceilToCents, checkBatchOutcome, checkSponsoredCallOutcome, classifyWalletFailure, combineTvlHistories, compareTrancheSeniority, computeSettlementWindow, countSubmissions, dailyInterest, decodeRevert, deriveCycleDates, derivePoolStatus, deriveRequestState, encodeCancelDepositCall, encodeCancelWithdrawalCall, encodeDepositData, encodeRequestWithdrawalCall, encodeStableTransfer, encodeWithdrawAtExpiryCall, epochAccrual, epochEndTimestamp, epochRateToApy, epochTrancheYield, explorerAddressUrl, explorerTxUrl, extractRevertData, fetchUnusedPoolIds, firstSubmissionTimestamp, floorToCents, formatSignTimestampUtc, getCreditOriginator, getInstitutionalLender, getStrategyRegion, getTrancheDisplayName, groupRiskManagementItems, isAuMinimumExempt, isAustralianKyc, isBelowMinimumCapacity, isCycleClosed, isDefinitivelyNotSubmitted, isFixedTermConfigId, isUnpredictableGas, isUserRejected, lastEventTimestamp, lockDurationDays, lockDurationMonths, maxNetRateCeiling, netEffectiveApy, netTrancheApyBounds, nextCycleBoundary, parseFacility, parseFormattedMessage, parseMinorUnits, parseTrancheBound, pickDefaultTrancheId, pickHighestYieldTranche, pickShortDescription, poolAllTranchesFull, poolMaxApy, poolTvl, projectedAnnualEarnings, proveDepositInReceipt, proveUserOperationInReceipt, readCmsStartDate, resolveBoundShortcuts, resolveDepositBounds, securityStructureOf, selectVisiblePools, splitBusinessTypes, splitNumberedClauses, strategyDeployments, strategyTotalsUsd, submissionEvents, submitBatch, submitSponsoredCall, trancheApyBounds, trancheHasCapacity, trancheRiskRank, validateDepositBatchCalls, weeklyInterest, weightedAverageApy };