@kasufinance/kasu-sdk 2.15.0 → 2.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -1
- package/dist/bundle.cjs.js +1144 -221
- package/dist/bundle.esm.js +1141 -222
- package/dist/domain/index.d.ts +2 -2
- package/dist/domain/index.js +1 -1
- package/dist/domain/index.js.map +1 -1
- package/dist/domain/yield-history.d.ts +25 -1
- package/dist/domain/yield-history.js +41 -11
- package/dist/domain/yield-history.js.map +1 -1
- package/dist/domain/yields.d.ts +200 -15
- package/dist/domain/yields.js +211 -14
- package/dist/domain/yields.js.map +1 -1
- package/dist/services/Locking/locking.d.ts +12 -0
- package/dist/services/Locking/locking.js +16 -0
- package/dist/services/Locking/locking.js.map +1 -1
- package/dist/services/Portfolio/portfolio.d.ts +54 -1
- package/dist/services/Portfolio/portfolio.js +357 -80
- package/dist/services/Portfolio/portfolio.js.map +1 -1
- package/dist/services/Portfolio/queries.d.ts +16 -0
- package/dist/services/Portfolio/queries.js +119 -2
- package/dist/services/Portfolio/queries.js.map +1 -1
- package/dist/services/Portfolio/types.d.ts +94 -6
- package/dist/services/Portfolio/yield-history.d.ts +148 -10
- package/dist/services/Portfolio/yield-history.js +241 -31
- package/dist/services/Portfolio/yield-history.js.map +1 -1
- package/dist/services/UserLending/user-lending.d.ts +16 -0
- package/dist/services/UserLending/user-lending.js +24 -0
- package/dist/services/UserLending/user-lending.js.map +1 -1
- package/dist/services/shared.d.ts +7 -0
- package/dist/services/shared.js +48 -0
- package/dist/services/shared.js.map +1 -1
- package/package.json +1 -1
- package/src/domain/index.ts +5 -1
- package/src/domain/yield-history.ts +47 -11
- package/src/domain/yields.ts +257 -15
- package/src/services/Locking/locking.ts +17 -0
- package/src/services/Portfolio/portfolio.ts +559 -126
- package/src/services/Portfolio/queries.ts +119 -2
- package/src/services/Portfolio/types.ts +91 -6
- package/src/services/Portfolio/yield-history.ts +405 -56
- package/src/services/UserLending/user-lending.ts +46 -0
- package/src/services/shared.ts +48 -0
package/src/domain/yields.ts
CHANGED
|
@@ -20,6 +20,17 @@
|
|
|
20
20
|
*/
|
|
21
21
|
export const SECONDS_PER_WEEK = 604_800;
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* The most unpaid interest {@link accruedInterestSince} shows for one pool:
|
|
25
|
+
* this many epochs of its weekly figure (owner ruling 2026-11-07).
|
|
26
|
+
*
|
|
27
|
+
* A pool can stop being cleared without being flagged stopped. On Base, one
|
|
28
|
+
* pool is more than six epochs behind and another more than a hundred.
|
|
29
|
+
* Uncapped, the figure would show interest that may never be credited. Four
|
|
30
|
+
* epochs covers any ordinary late clearing.
|
|
31
|
+
*/
|
|
32
|
+
export const MAX_ACCRUAL_EPOCHS = 4;
|
|
33
|
+
|
|
23
34
|
/** One slice of a position that earns at a single rate. */
|
|
24
35
|
export interface InterestSlice {
|
|
25
36
|
/** The lender's current balance in this slice, in stable display units. */
|
|
@@ -37,7 +48,13 @@ export interface InterestSlice {
|
|
|
37
48
|
epochRate: number;
|
|
38
49
|
}
|
|
39
50
|
|
|
40
|
-
/**
|
|
51
|
+
/**
|
|
52
|
+
* One pool's contribution to the current cycle's accrual, anchored on its
|
|
53
|
+
* last clearing TRANSACTION.
|
|
54
|
+
*
|
|
55
|
+
* @deprecated Use {@link EpochAccrual} with {@link accruedInterestSince}. A
|
|
56
|
+
* clearing transaction's timestamp is the wrong anchor: see `accruedInterest`.
|
|
57
|
+
*/
|
|
41
58
|
export interface PoolAccrual {
|
|
42
59
|
/** That pool's projected net interest for one epoch (`weeklyInterest`). */
|
|
43
60
|
weeklyInterest: number;
|
|
@@ -45,6 +62,29 @@ export interface PoolAccrual {
|
|
|
45
62
|
clearingTimestamp: number;
|
|
46
63
|
}
|
|
47
64
|
|
|
65
|
+
/**
|
|
66
|
+
* One pool's contribution to the unpaid interest — the input of
|
|
67
|
+
* {@link accruedInterestSince}. Build it with {@link epochAccrual}, which
|
|
68
|
+
* validates every field and derives the two timestamps; do not assemble one
|
|
69
|
+
* by hand.
|
|
70
|
+
*/
|
|
71
|
+
export interface EpochAccrual {
|
|
72
|
+
/** That pool's projected net interest for one epoch (`weeklyInterest`). */
|
|
73
|
+
weeklyInterest: number;
|
|
74
|
+
/**
|
|
75
|
+
* Unix SECONDS from which the pool's unpaid interest accrues: the earlier
|
|
76
|
+
* of the moment its last clearing STARTED and `epochEnd`. Never after
|
|
77
|
+
* `epochEnd`.
|
|
78
|
+
*/
|
|
79
|
+
accruesFrom: number;
|
|
80
|
+
/**
|
|
81
|
+
* Unix SECONDS at which the pool's last cleared epoch ENDS
|
|
82
|
+
* (`epochEndTimestamp(initialEpochStart, lastClearedEpoch)`). The accrual
|
|
83
|
+
* holds from `accruesFrom + 604 800` until `epochEnd + 604 800`.
|
|
84
|
+
*/
|
|
85
|
+
epochEnd: number;
|
|
86
|
+
}
|
|
87
|
+
|
|
48
88
|
function isUsableFeePercent(feePercent: number | null): feePercent is number {
|
|
49
89
|
return (
|
|
50
90
|
feePercent !== null &&
|
|
@@ -102,28 +142,40 @@ export function dailyInterest(weekly: number | null): number | null {
|
|
|
102
142
|
}
|
|
103
143
|
|
|
104
144
|
/**
|
|
105
|
-
* Interest accrued since each pool's OWN last
|
|
106
|
-
*
|
|
145
|
+
* Interest accrued since each pool's OWN last clearing transaction, capped at
|
|
146
|
+
* one epoch per pool.
|
|
107
147
|
*
|
|
108
148
|
* ```
|
|
109
149
|
* accrued = Σ_pool weekly_pool × clamp(now − clearing_pool, 0, 604 800) / 604 800
|
|
110
150
|
* ```
|
|
111
151
|
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
* per pool, the total is monotonic across any single pool's clearing: that
|
|
116
|
-
* pool swaps its accrued share for a capitalised one, and no other term moves.
|
|
152
|
+
* Per pool, because pools clear independently: one shared anchor accrues a
|
|
153
|
+
* stale pool's share from a close it never had, and when another pool clears
|
|
154
|
+
* it drops the total by the stale pool's whole weekly share.
|
|
117
155
|
*
|
|
118
|
-
* **The one-epoch cap
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
156
|
+
* **The one-epoch cap rested on a wrong premise.** It assumed a late
|
|
157
|
+
* clearing credits only one epoch, so accruing past seven days would promise
|
|
158
|
+
* interest no close pays. The contracts say otherwise. `ClearingCoordinator`
|
|
159
|
+
* states that clearing "must be executed once per epoch for each lending
|
|
160
|
+
* pool" and that "clearing epochs must be executed in order"; `doClearing`
|
|
161
|
+
* reverts unless the target epoch is the pool's `nextLendingPoolClearingEpoch`,
|
|
162
|
+
* and it calls `LendingPool.applyInterests(targetEpoch)` on every clearing,
|
|
163
|
+
* late or not ("if clearing for the epoch is executed only after the clearing
|
|
164
|
+
* period, only yield will be applied"). Every missed epoch's interest is
|
|
165
|
+
* therefore credited when the clearings catch up — on Base, one pool cleared
|
|
166
|
+
* epochs 116 to 119 in the same block, four weeks credited at once.
|
|
167
|
+
* Capped, the figure freezes as soon as a clearing is a week late; anchored
|
|
168
|
+
* on the transaction, it also DIPS on a catch-up (elapsed resets to zero
|
|
169
|
+
* while only one week is credited per clearing).
|
|
123
170
|
*
|
|
171
|
+
* The low clamp makes a close ahead of the local clock zero, never negative.
|
|
124
172
|
* A non-finite term contributes nothing; a non-finite `nowSeconds` accrues
|
|
125
|
-
* nothing.
|
|
126
|
-
*
|
|
173
|
+
* nothing.
|
|
174
|
+
*
|
|
175
|
+
* @deprecated Use {@link accruedInterestSince} with {@link epochAccrual}:
|
|
176
|
+
* identical while clearings run on time, and it keeps accruing, up to four
|
|
177
|
+
* epochs, when they run late. Kept, behaviour-identical, for existing
|
|
178
|
+
* callers.
|
|
127
179
|
*/
|
|
128
180
|
export function accruedInterest(
|
|
129
181
|
accruals: readonly PoolAccrual[],
|
|
@@ -144,6 +196,196 @@ export function accruedInterest(
|
|
|
144
196
|
return accrued;
|
|
145
197
|
}
|
|
146
198
|
|
|
199
|
+
/**
|
|
200
|
+
* Unix SECONDS at which `epoch` ENDS — the start of the next one:
|
|
201
|
+
*
|
|
202
|
+
* ```
|
|
203
|
+
* epochEnd(e) = initialEpochStart + (e + 1) × 604 800
|
|
204
|
+
* ```
|
|
205
|
+
*
|
|
206
|
+
* `SystemVariables.epochStartTimestamp(e)` is `initial + e × EPOCH_DURATION`,
|
|
207
|
+
* and the initial start is written once, at initialisation, so the end of any
|
|
208
|
+
* epoch follows from `epochStartTimestamp(0)` with no further read.
|
|
209
|
+
*
|
|
210
|
+
* FAILS CLOSED with `NaN`: a non-integer, non-finite or negative epoch, an
|
|
211
|
+
* initial start that is not a positive integer, or a result past
|
|
212
|
+
* `Number.MAX_SAFE_INTEGER`.
|
|
213
|
+
*
|
|
214
|
+
* @param initialEpochStart unix seconds, `epochStartTimestamp(0)`.
|
|
215
|
+
* @param epoch the epoch number, 0-based.
|
|
216
|
+
*/
|
|
217
|
+
export function epochEndTimestamp(
|
|
218
|
+
initialEpochStart: number,
|
|
219
|
+
epoch: number,
|
|
220
|
+
): number {
|
|
221
|
+
if (!Number.isSafeInteger(initialEpochStart) || initialEpochStart <= 0) {
|
|
222
|
+
return NaN;
|
|
223
|
+
}
|
|
224
|
+
if (!Number.isSafeInteger(epoch) || epoch < 0) return NaN;
|
|
225
|
+
const end = initialEpochStart + (epoch + 1) * SECONDS_PER_WEEK;
|
|
226
|
+
return Number.isSafeInteger(end) ? end : NaN;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Builds one pool's {@link EpochAccrual} from what the chain and the subgraph
|
|
231
|
+
* say about its last clearing:
|
|
232
|
+
*
|
|
233
|
+
* ```
|
|
234
|
+
* epochEnd = epochEndTimestamp(initialEpochStart, lastClearedEpoch)
|
|
235
|
+
* accruesFrom = min(clearingStartedAt, epochEnd)
|
|
236
|
+
* ```
|
|
237
|
+
*
|
|
238
|
+
* - **On time** (the clearing started inside its window, before its epoch
|
|
239
|
+
* ended): `accruesFrom` is the clearing's start. Clearing applies the
|
|
240
|
+
* epoch's interest first and only then accepts requests, so today's
|
|
241
|
+
* balance — which includes a deposit that clearing accepted — is never
|
|
242
|
+
* charged for time before it: a new lender starts at exactly 0.
|
|
243
|
+
* - **Late** (it started at or after its epoch end): `accruesFrom` is the
|
|
244
|
+
* epoch end. A late clearing accepts no requests and credits exactly one
|
|
245
|
+
* epoch, so the time it did not credit is kept.
|
|
246
|
+
*
|
|
247
|
+
* `lastClearedEpoch` is the newest epoch whose clearing has STARTED (the
|
|
248
|
+
* subgraph has a `LendingPoolClearing` for it), not the newest that has
|
|
249
|
+
* finished, and `clearingStartedAt` is that clearing's `startTimestamp`.
|
|
250
|
+
* `ClearingCoordinator.doClearing` mints the epoch's interest in its FIRST
|
|
251
|
+
* transaction (`applyInterests`), while `nextLendingPoolClearingEpoch` only
|
|
252
|
+
* advances when the clearing ENDS, so it is the wrong source.
|
|
253
|
+
*
|
|
254
|
+
* FAILS CLOSED with `null`: whatever {@link epochEndTimestamp} rejects, a
|
|
255
|
+
* `clearingStartedAt` that is not a finite positive number, or a
|
|
256
|
+
* `weeklyInterest` that is not finite or is negative. The caller then offers
|
|
257
|
+
* no term for that pool.
|
|
258
|
+
*
|
|
259
|
+
* @param input.weeklyInterest the pool's net interest for one epoch.
|
|
260
|
+
* @param input.initialEpochStart unix seconds, `epochStartTimestamp(0)`.
|
|
261
|
+
* @param input.lastClearedEpoch the newest epoch whose clearing has started.
|
|
262
|
+
* @param input.clearingStartedAt unix seconds, that clearing's start.
|
|
263
|
+
*/
|
|
264
|
+
export function epochAccrual(input: {
|
|
265
|
+
weeklyInterest: number;
|
|
266
|
+
initialEpochStart: number;
|
|
267
|
+
lastClearedEpoch: number;
|
|
268
|
+
clearingStartedAt: number;
|
|
269
|
+
}): EpochAccrual | null {
|
|
270
|
+
const { weeklyInterest: weekly, clearingStartedAt } = input;
|
|
271
|
+
if (!Number.isFinite(weekly) || weekly < 0) return null;
|
|
272
|
+
if (!Number.isFinite(clearingStartedAt) || clearingStartedAt <= 0) {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
const epochEnd = epochEndTimestamp(
|
|
276
|
+
input.initialEpochStart,
|
|
277
|
+
input.lastClearedEpoch,
|
|
278
|
+
);
|
|
279
|
+
if (Number.isNaN(epochEnd)) return null;
|
|
280
|
+
return {
|
|
281
|
+
weeklyInterest: weekly,
|
|
282
|
+
accruesFrom: Math.min(clearingStartedAt, epochEnd),
|
|
283
|
+
epochEnd,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Interest accrued and not yet credited — the moving part of lifetime
|
|
289
|
+
* interest (lifetime = settled on chain + this). Per term, with
|
|
290
|
+
* `W` = 604 800 s:
|
|
291
|
+
*
|
|
292
|
+
* ```
|
|
293
|
+
* accrued = min( weekly × ( min(max(now − accruesFrom, 0), W)
|
|
294
|
+
* + max(now − (epochEnd + W), 0) ) / W,
|
|
295
|
+
* MAX_ACCRUAL_EPOCHS × weekly )
|
|
296
|
+
* ```
|
|
297
|
+
*
|
|
298
|
+
* Read along the clock, for one pool:
|
|
299
|
+
* 1. from `accruesFrom`, one epoch of interest accrues linearly;
|
|
300
|
+
* 2. it then HOLDS from `accruesFrom + W` until `epochEnd + W`, the end of
|
|
301
|
+
* the next epoch — at most 48 h after an on-time clearing (its clearing
|
|
302
|
+
* window), no time at all after a late one (`accruesFrom == epochEnd`);
|
|
303
|
+
* 3. then it resumes, up to {@link MAX_ACCRUAL_EPOCHS} (4) epochs of
|
|
304
|
+
* `weekly` in all, and stops there.
|
|
305
|
+
*
|
|
306
|
+
* **Why it keeps accruing past one epoch.** Clearing must run once per
|
|
307
|
+
* epoch for each pool, in order (`ClearingCoordinator.doClearing` reverts
|
|
308
|
+
* unless the target epoch is the pool's `nextLendingPoolClearingEpoch`), and
|
|
309
|
+
* every clearing, on time or late, starts with
|
|
310
|
+
* `LendingPool.applyInterests(targetEpoch)`. A missed epoch is therefore
|
|
311
|
+
* credited when the clearings catch up (on Base, one pool cleared epochs 116
|
|
312
|
+
* to 119 in the same block).
|
|
313
|
+
*
|
|
314
|
+
* **Why it stops at four epochs** (owner ruling 2026-11-07). A pool can stop
|
|
315
|
+
* being cleared without being flagged stopped. On Base today one pool is
|
|
316
|
+
* more than six epochs behind and another more than a hundred. Past four
|
|
317
|
+
* epochs the figure would show interest that may never be credited. Four
|
|
318
|
+
* covers any ordinary late clearing.
|
|
319
|
+
*
|
|
320
|
+
* **Why the hold.** Without it an on-time anchor accrues past one epoch
|
|
321
|
+
* while the next on-time clearing — which credits exactly one — is still to
|
|
322
|
+
* come, and the total steps DOWN when it lands. With it, at a constant
|
|
323
|
+
* balance, `settled + accrued` never steps down at a clearing (`t` = the
|
|
324
|
+
* NEW clearing's start, `E` = the end of the epoch it clears):
|
|
325
|
+
* - on time after on time: identical to the deprecated `accruedInterest`
|
|
326
|
+
* at every instant. With `gap` = `t` − the previous clearing's start, it
|
|
327
|
+
* steps UP by `weekly × (W − gap) / W` when `gap < W` (at most 2/7 of one
|
|
328
|
+
* weekly, as before), and not at all when `gap ≥ W`;
|
|
329
|
+
* - late after on time, and late after late: exactly continuous;
|
|
330
|
+
* - on time after late: steps UP by `weekly × (E − t) / W`, at most 2/7 of
|
|
331
|
+
* one weekly.
|
|
332
|
+
* A deposit or top-up accepted at an on-time clearing starts at 0, so it
|
|
333
|
+
* never dips the total either.
|
|
334
|
+
*
|
|
335
|
+
* **At the cap.** All of the above holds for a pool below the cap, where
|
|
336
|
+
* the cap changes nothing. A pool AT the cap shows `4 × weekly` before a
|
|
337
|
+
* catch-up clearing; after it, one more `weekly` is settled and the new term
|
|
338
|
+
* shows `min(elapsed′, 4 × weekly)`, so `after = min(elapsed′ + weekly,
|
|
339
|
+
* 5 × weekly) ≥ before`. Each clearing steps the total UP by at most one
|
|
340
|
+
* weekly until the pool is back under the cap. Never down, and never above
|
|
341
|
+
* settled plus `4 × weekly` per pool.
|
|
342
|
+
*
|
|
343
|
+
* **Never negative**, and non-decreasing in `now`. A term is skipped — it
|
|
344
|
+
* contributes nothing — when `weeklyInterest` is not finite or is negative,
|
|
345
|
+
* when `accruesFrom` or `epochEnd` is not finite, when `accruesFrom >
|
|
346
|
+
* epochEnd` (inconsistent: the anchor is never after the epoch end), or when
|
|
347
|
+
* an elapsed time overflows. A non-finite `nowSeconds` accrues nothing.
|
|
348
|
+
*
|
|
349
|
+
* **Per pool, because pools clear independently.** One shared anchor accrues
|
|
350
|
+
* a stale pool's share from a close it never had.
|
|
351
|
+
*
|
|
352
|
+
* **Accepted approximation.** `weekly` is today's balance at today's epoch
|
|
353
|
+
* rate. Over a multi-week gap the chain compounds weekly and may apply other
|
|
354
|
+
* epochs' rates, so a long accrual reads slightly LOW. It is a ticking
|
|
355
|
+
* estimate, not a ledger.
|
|
356
|
+
*
|
|
357
|
+
* Callers fail closed upstream: no term for a pool whose weekly interest or
|
|
358
|
+
* clearing is unknown (`epochAccrual` returns `null`), and none for a pool
|
|
359
|
+
* that has stopped — it will never clear again.
|
|
360
|
+
*/
|
|
361
|
+
export function accruedInterestSince(
|
|
362
|
+
accruals: readonly EpochAccrual[],
|
|
363
|
+
nowSeconds: number,
|
|
364
|
+
): number {
|
|
365
|
+
if (!Number.isFinite(nowSeconds)) return 0;
|
|
366
|
+
let accrued = 0;
|
|
367
|
+
for (const { weeklyInterest: weekly, accruesFrom, epochEnd } of accruals) {
|
|
368
|
+
if (!Number.isFinite(weekly) || weekly < 0) continue;
|
|
369
|
+
if (!Number.isFinite(accruesFrom) || !Number.isFinite(epochEnd)) {
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
if (accruesFrom > epochEnd) continue;
|
|
373
|
+
const sinceAnchor = nowSeconds - accruesFrom;
|
|
374
|
+
const sinceHoldEnd = nowSeconds - (epochEnd + SECONDS_PER_WEEK);
|
|
375
|
+
if (!Number.isFinite(sinceAnchor) || !Number.isFinite(sinceHoldEnd)) {
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
const elapsed =
|
|
379
|
+
Math.min(Math.max(sinceAnchor, 0), SECONDS_PER_WEEK) +
|
|
380
|
+
Math.max(sinceHoldEnd, 0);
|
|
381
|
+
accrued += Math.min(
|
|
382
|
+
(weekly * elapsed) / SECONDS_PER_WEEK,
|
|
383
|
+
MAX_ACCRUAL_EPOCHS * weekly,
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
return accrued;
|
|
387
|
+
}
|
|
388
|
+
|
|
147
389
|
/**
|
|
148
390
|
* What `amount` earns in a year at a NET rate — the "≈ $Y a year" line.
|
|
149
391
|
* Feed it `netEffectiveApy(...)`, never a gross APY: the result is a lender's
|
|
@@ -625,6 +625,23 @@ export class KSULocking {
|
|
|
625
625
|
).toNumber();
|
|
626
626
|
}
|
|
627
627
|
|
|
628
|
+
/**
|
|
629
|
+
* Unix SECONDS at which epoch 0 started:
|
|
630
|
+
* `SystemVariables.epochStartTimestamp(0)`. The contract writes this once,
|
|
631
|
+
* at initialisation, so it never changes for a deployment and may be cached
|
|
632
|
+
* for the life of the app. Every epoch boundary follows from it —
|
|
633
|
+
* `epochEndTimestamp(initial, epoch)` and `epochAccrual` in the domain
|
|
634
|
+
* layer.
|
|
635
|
+
*
|
|
636
|
+
* Available on every deployment, Full and Lite. A failed read rejects; the
|
|
637
|
+
* caller validates the value (a positive integer) before using it.
|
|
638
|
+
*/
|
|
639
|
+
async getInitialEpochStartTimestamp(): Promise<number> {
|
|
640
|
+
return (
|
|
641
|
+
await this._systemVariablesAbi.epochStartTimestamp(0)
|
|
642
|
+
).toNumber();
|
|
643
|
+
}
|
|
644
|
+
|
|
628
645
|
async getNextClearingPeriodDate(): Promise<EpochTimeStamp> {
|
|
629
646
|
const nextEpochDate = await this.getNextEpochDate();
|
|
630
647
|
|