@kasufinance/kasu-sdk 2.14.0 → 2.16.0

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