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