@kasufinance/kasu-sdk 2.13.1 → 2.15.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 +68 -5
- package/dist/bundle.cjs.js +1038 -29
- package/dist/bundle.esm.js +1026 -30
- package/dist/domain/index.d.ts +8 -3
- package/dist/domain/index.js +6 -2
- package/dist/domain/index.js.map +1 -1
- package/dist/domain/loan-contract.d.ts +16 -5
- package/dist/domain/loan-contract.js +16 -5
- package/dist/domain/loan-contract.js.map +1 -1
- package/dist/domain/pool-tvl.d.ts +104 -0
- package/dist/domain/pool-tvl.js +179 -0
- package/dist/domain/pool-tvl.js.map +1 -1
- package/dist/domain/requests.js +29 -19
- package/dist/domain/requests.js.map +1 -1
- package/dist/domain/strategy-details.d.ts +262 -0
- package/dist/domain/strategy-details.js +375 -0
- package/dist/domain/strategy-details.js.map +1 -0
- package/dist/facade/index.d.ts +3 -1
- package/dist/facade/index.js +2 -0
- package/dist/facade/index.js.map +1 -1
- package/dist/facade/strategies.d.ts +102 -4
- package/dist/facade/strategies.js +182 -6
- package/dist/facade/strategies.js.map +1 -1
- package/dist/facade/strategy-deployments.d.ts +65 -0
- package/dist/facade/strategy-deployments.js +97 -0
- package/dist/facade/strategy-deployments.js.map +1 -0
- package/dist/facade/types.d.ts +82 -0
- package/dist/flows/deposit-flow.d.ts +19 -5
- package/dist/flows/deposit-flow.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js.map +1 -1
- package/dist/services/DataService/data-service.d.ts +29 -1
- package/dist/services/DataService/data-service.js +80 -9
- package/dist/services/DataService/data-service.js.map +1 -1
- package/dist/services/DataService/tvl-history.d.ts +49 -0
- package/dist/services/DataService/tvl-history.js +89 -0
- package/dist/services/DataService/tvl-history.js.map +1 -1
- package/dist/services/UserLending/queries.js +2 -7
- package/dist/services/UserLending/queries.js.map +1 -1
- package/dist/services/UserLending/subgraph-types.js.map +1 -1
- package/dist/services/UserLending/types.js.map +1 -1
- package/dist/services/UserLending/user-lending.js +72 -16
- package/dist/services/UserLending/user-lending.js.map +1 -1
- package/package.json +1 -1
- package/src/domain/index.ts +34 -3
- package/src/domain/loan-contract.ts +16 -5
- package/src/domain/pool-tvl.ts +281 -0
- package/src/domain/strategy-details.ts +528 -0
- package/src/facade/index.ts +13 -0
- package/src/facade/strategies.ts +231 -11
- package/src/facade/strategy-deployments.ts +161 -0
- package/src/facade/types.ts +85 -0
- package/src/flows/deposit-flow.ts +19 -5
- package/src/index.ts +4 -1
- package/src/services/DataService/data-service.ts +103 -7
- package/src/services/DataService/tvl-history.ts +131 -0
package/dist/bundle.cjs.js
CHANGED
|
@@ -15502,6 +15502,185 @@ KasuPoolExternalTVLAbi__factory.abi = _abi;
|
|
|
15502
15502
|
function poolTvl(onChainBalance, offChainTvl) {
|
|
15503
15503
|
return parseFloat(onChainBalance) + parseFloat(offChainTvl);
|
|
15504
15504
|
}
|
|
15505
|
+
/**
|
|
15506
|
+
* What `combineTvlHistories` throws instead of returning a number it cannot
|
|
15507
|
+
* stand behind. `leg` and `entry` are the zero-based position of the
|
|
15508
|
+
* offending input, when there is one.
|
|
15509
|
+
*/
|
|
15510
|
+
class TvlHistoryInputError extends Error {
|
|
15511
|
+
constructor(code, leg, entry) {
|
|
15512
|
+
const at = [
|
|
15513
|
+
leg === undefined ? '' : ` at leg ${String(leg)}`,
|
|
15514
|
+
entry === undefined ? '' : `, entry ${String(entry)}`,
|
|
15515
|
+
].join('');
|
|
15516
|
+
super(`combineTvlHistories: input refused (${code})${at}`);
|
|
15517
|
+
this.name = 'TvlHistoryInputError';
|
|
15518
|
+
this.code = code;
|
|
15519
|
+
this.leg = leg;
|
|
15520
|
+
this.entry = entry;
|
|
15521
|
+
Object.setPrototypeOf(this, TvlHistoryInputError.prototype);
|
|
15522
|
+
}
|
|
15523
|
+
}
|
|
15524
|
+
function isNonNegativeFinite(value) {
|
|
15525
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= 0;
|
|
15526
|
+
}
|
|
15527
|
+
/**
|
|
15528
|
+
* Throws unless the leg is well formed. Index loops, not `forEach`: a hole in
|
|
15529
|
+
* a sparse array is skipped by the array methods and would surface later as
|
|
15530
|
+
* a raw `TypeError` instead of a refusal.
|
|
15531
|
+
*/
|
|
15532
|
+
function validateLeg(leg, legIndex) {
|
|
15533
|
+
if (typeof leg !== 'object' || leg === null) {
|
|
15534
|
+
throw new TvlHistoryInputError('leg', legIndex);
|
|
15535
|
+
}
|
|
15536
|
+
const { entries, usdRate, opening } = leg;
|
|
15537
|
+
if (typeof usdRate !== 'number' ||
|
|
15538
|
+
!Number.isFinite(usdRate) ||
|
|
15539
|
+
usdRate <= 0) {
|
|
15540
|
+
throw new TvlHistoryInputError('usd-rate', legIndex);
|
|
15541
|
+
}
|
|
15542
|
+
if (!Array.isArray(entries)) {
|
|
15543
|
+
throw new TvlHistoryInputError('entries', legIndex);
|
|
15544
|
+
}
|
|
15545
|
+
const list = entries;
|
|
15546
|
+
if (list.length === 0) {
|
|
15547
|
+
throw new TvlHistoryInputError('empty-leg', legIndex);
|
|
15548
|
+
}
|
|
15549
|
+
let previous = -Infinity;
|
|
15550
|
+
for (let index = 0; index < list.length; index += 1) {
|
|
15551
|
+
const entry = list[index];
|
|
15552
|
+
if (typeof entry !== 'object' || entry === null) {
|
|
15553
|
+
throw new TvlHistoryInputError('entry', legIndex, index);
|
|
15554
|
+
}
|
|
15555
|
+
const { timestamp, tvl, kind } = entry;
|
|
15556
|
+
if (!isNonNegativeFinite(timestamp)) {
|
|
15557
|
+
throw new TvlHistoryInputError('timestamp', legIndex, index);
|
|
15558
|
+
}
|
|
15559
|
+
if (!isNonNegativeFinite(tvl)) {
|
|
15560
|
+
throw new TvlHistoryInputError('tvl', legIndex, index);
|
|
15561
|
+
}
|
|
15562
|
+
if (kind !== 'clearing' && kind !== 'current') {
|
|
15563
|
+
throw new TvlHistoryInputError('kind', legIndex, index);
|
|
15564
|
+
}
|
|
15565
|
+
if (kind === 'current' && index !== list.length - 1) {
|
|
15566
|
+
throw new TvlHistoryInputError('current', legIndex, index);
|
|
15567
|
+
}
|
|
15568
|
+
if (timestamp < previous) {
|
|
15569
|
+
throw new TvlHistoryInputError('order', legIndex, index);
|
|
15570
|
+
}
|
|
15571
|
+
previous = timestamp;
|
|
15572
|
+
}
|
|
15573
|
+
if (opening !== undefined && opening !== null) {
|
|
15574
|
+
const first = list[0];
|
|
15575
|
+
const { timestamp, tvl, kind } = (typeof opening === 'object' ? opening : {});
|
|
15576
|
+
if (!isNonNegativeFinite(timestamp) ||
|
|
15577
|
+
!isNonNegativeFinite(tvl) ||
|
|
15578
|
+
kind !== 'clearing' ||
|
|
15579
|
+
timestamp > first.timestamp) {
|
|
15580
|
+
throw new TvlHistoryInputError('opening', legIndex);
|
|
15581
|
+
}
|
|
15582
|
+
}
|
|
15583
|
+
return leg;
|
|
15584
|
+
}
|
|
15585
|
+
/**
|
|
15586
|
+
* A strategy's TVL in USD over time, from its deployments' histories — Tax
|
|
15587
|
+
* Pay runs on Base (USDC) and on XDC (AUDD), and its total lent is both.
|
|
15588
|
+
* Pure: the caller fetches each leg (`strategyDeployments` names them) and
|
|
15589
|
+
* passes the exchange rates.
|
|
15590
|
+
*
|
|
15591
|
+
* A step merge by time. At every distinct timestamp where any leg has a
|
|
15592
|
+
* clearing, the value is the sum over legs of that leg's latest point at or
|
|
15593
|
+
* before then, times its `usdRate`. A leg's `opening` — what it held just
|
|
15594
|
+
* before the window — is one more step: it counts from its own timestamp
|
|
15595
|
+
* until the leg's first entry, and the leg is 0 before it (or throughout,
|
|
15596
|
+
* before its first entry, without one).
|
|
15597
|
+
* Points that share a timestamp — clearings run in one block, or two chains
|
|
15598
|
+
* clearing in the same second — are one output point. The series ends with
|
|
15599
|
+
* one `current` point at the latest of the legs' `current` timestamps, equal
|
|
15600
|
+
* to the sum of their `current` values. Every leg must end in a `current`
|
|
15601
|
+
* point, or none may.
|
|
15602
|
+
*
|
|
15603
|
+
* The window is the union of the legs' windows: the series starts at the
|
|
15604
|
+
* earliest first entry of any leg. An `opening` is a value, never a point.
|
|
15605
|
+
* Without it, two windows cut at offset clearing times would count the
|
|
15606
|
+
* later-starting leg 0 at the left edge and draw a false step up; with it,
|
|
15607
|
+
* only a genuine late start (`opening: null`) counts 0.
|
|
15608
|
+
*
|
|
15609
|
+
* Every leg and entry is validated first: a bad input — an empty leg
|
|
15610
|
+
* included — throws `TvlHistoryInputError` (its `code` says why), so the
|
|
15611
|
+
* result never holds `NaN` or `Infinity`. The inputs are not modified.
|
|
15612
|
+
*
|
|
15613
|
+
* ```ts
|
|
15614
|
+
* const window = { count: 52 };
|
|
15615
|
+
* const points = combineTvlHistories([
|
|
15616
|
+
* { ...(await base.strategies.getPoolTvlHistoryWindow(basePool, window)), usdRate: 1 },
|
|
15617
|
+
* { ...(await xdc.strategies.getPoolTvlHistoryWindow(xdcPool, window)), usdRate: audToUsd },
|
|
15618
|
+
* ]);
|
|
15619
|
+
* ```
|
|
15620
|
+
*/
|
|
15621
|
+
function combineTvlHistories(legs) {
|
|
15622
|
+
if (!Array.isArray(legs) || legs.length === 0) {
|
|
15623
|
+
throw new TvlHistoryInputError('legs');
|
|
15624
|
+
}
|
|
15625
|
+
const valid = [];
|
|
15626
|
+
for (let index = 0; index < legs.length; index += 1) {
|
|
15627
|
+
valid.push(validateLeg(legs[index], index));
|
|
15628
|
+
}
|
|
15629
|
+
const endsInCurrent = valid.map((leg) => leg.entries[leg.entries.length - 1].kind === 'current');
|
|
15630
|
+
const odd = endsInCurrent.findIndex((ends) => ends !== endsInCurrent[0]);
|
|
15631
|
+
if (odd !== -1)
|
|
15632
|
+
throw new TvlHistoryInputError('current', odd);
|
|
15633
|
+
const finalTimestamp = endsInCurrent[0]
|
|
15634
|
+
? Math.max(...valid.map((leg) => leg.entries[leg.entries.length - 1].timestamp))
|
|
15635
|
+
: undefined;
|
|
15636
|
+
const clearingTimes = [
|
|
15637
|
+
...new Set(valid.flatMap((leg) => leg.entries
|
|
15638
|
+
.filter((entry) => entry.kind === 'clearing')
|
|
15639
|
+
.map((entry) => entry.timestamp))),
|
|
15640
|
+
]
|
|
15641
|
+
.filter((t) => finalTimestamp === undefined || t < finalTimestamp)
|
|
15642
|
+
.sort((a, b) => a - b);
|
|
15643
|
+
// One cursor per leg: the times ascend, so each leg is walked once. A
|
|
15644
|
+
// leg's `current` point is a step like any other, which keeps a chain
|
|
15645
|
+
// whose subgraph lags behind the other's last clearing at its own latest.
|
|
15646
|
+
const cursors = valid.map(() => -1);
|
|
15647
|
+
const sumAt = (t) => {
|
|
15648
|
+
let sum = 0;
|
|
15649
|
+
valid.forEach((leg, index) => {
|
|
15650
|
+
let cursor = cursors[index];
|
|
15651
|
+
while (cursor + 1 < leg.entries.length &&
|
|
15652
|
+
leg.entries[cursor + 1].timestamp <= t) {
|
|
15653
|
+
cursor += 1;
|
|
15654
|
+
}
|
|
15655
|
+
cursors[index] = cursor;
|
|
15656
|
+
// Before the first entry, the opening is the step — from its
|
|
15657
|
+
// own time on, never earlier.
|
|
15658
|
+
const opening = leg.opening;
|
|
15659
|
+
const value = cursor >= 0
|
|
15660
|
+
? leg.entries[cursor].tvl
|
|
15661
|
+
: opening && opening.timestamp <= t
|
|
15662
|
+
? opening.tvl
|
|
15663
|
+
: 0;
|
|
15664
|
+
sum += value * leg.usdRate;
|
|
15665
|
+
});
|
|
15666
|
+
if (!Number.isFinite(sum))
|
|
15667
|
+
throw new TvlHistoryInputError('overflow');
|
|
15668
|
+
return sum;
|
|
15669
|
+
};
|
|
15670
|
+
const combined = clearingTimes.map((t) => ({
|
|
15671
|
+
timestamp: t,
|
|
15672
|
+
tvlUsd: sumAt(t),
|
|
15673
|
+
kind: 'clearing',
|
|
15674
|
+
}));
|
|
15675
|
+
if (finalTimestamp !== undefined) {
|
|
15676
|
+
combined.push({
|
|
15677
|
+
timestamp: finalTimestamp,
|
|
15678
|
+
tvlUsd: sumAt(finalTimestamp),
|
|
15679
|
+
kind: 'current',
|
|
15680
|
+
});
|
|
15681
|
+
}
|
|
15682
|
+
return combined;
|
|
15683
|
+
}
|
|
15505
15684
|
|
|
15506
15685
|
/**
|
|
15507
15686
|
* Epochs in a year — the single source of truth for every rate conversion in
|
|
@@ -16250,6 +16429,95 @@ function fetchPoolClearings(request, poolId, scope, cancellation) {
|
|
|
16250
16429
|
return { clearings, snapshot, requests };
|
|
16251
16430
|
});
|
|
16252
16431
|
}
|
|
16432
|
+
/**
|
|
16433
|
+
* Pure: the opening clearing — the newest of `before` (finished clearings the
|
|
16434
|
+
* window left out, older by epoch) that finished STRICTLY before the first
|
|
16435
|
+
* in-window clearing's block. Newest by block, then epoch, so a clearing
|
|
16436
|
+
* numbered out of order (XDC AUDD's epoch 1, a week before epoch 88) is
|
|
16437
|
+
* placed by when it happened. A clearing in the first in-window block itself
|
|
16438
|
+
* (a catch-up block that finished several epochs at once) is not an opening:
|
|
16439
|
+
* it holds the first entry's own value, not the one before it. `null` when
|
|
16440
|
+
* there is none — see `needsClearingBeforeBlock`.
|
|
16441
|
+
*/
|
|
16442
|
+
function pickOpeningClearing(before, inWindow) {
|
|
16443
|
+
const firstBlock = Math.min(...inWindow.map((c) => c.block));
|
|
16444
|
+
let opening = null;
|
|
16445
|
+
for (const clearing of before) {
|
|
16446
|
+
if (clearing.block >= firstBlock)
|
|
16447
|
+
continue;
|
|
16448
|
+
if (opening === null ||
|
|
16449
|
+
clearing.block > opening.block ||
|
|
16450
|
+
(clearing.block === opening.block &&
|
|
16451
|
+
clearing.epochId > opening.epochId)) {
|
|
16452
|
+
opening = clearing;
|
|
16453
|
+
}
|
|
16454
|
+
}
|
|
16455
|
+
return opening;
|
|
16456
|
+
}
|
|
16457
|
+
/**
|
|
16458
|
+
* Pure: whether the opening needs `fetchClearingBefore`'s by-block read.
|
|
16459
|
+
* Only when the window left clearings out but every one of them finished in
|
|
16460
|
+
* or after the first in-window block — the extra rows sat in a catch-up
|
|
16461
|
+
* block — so an older one, if any, was not fetched. Otherwise the opening
|
|
16462
|
+
* (or its absence) is already known and nothing more is read.
|
|
16463
|
+
*/
|
|
16464
|
+
function needsClearingBeforeBlock(before, opening) {
|
|
16465
|
+
return opening === null && before.length > 0;
|
|
16466
|
+
}
|
|
16467
|
+
/**
|
|
16468
|
+
* The newest finished clearing in a block below `blockLt`, by block: the
|
|
16469
|
+
* opening when every clearing the window left out shares its first block.
|
|
16470
|
+
* ONE extra request for one row, no snapshot. Clearings that share that
|
|
16471
|
+
* older block share its time and balance, so which one comes back does not
|
|
16472
|
+
* change the value.
|
|
16473
|
+
*/
|
|
16474
|
+
const poolTvlClearingBeforeBlockQuery = gql `
|
|
16475
|
+
query PoolTvlClearingBeforeBlock($pool: String!, $blockLt: BigInt!) {
|
|
16476
|
+
clearings: lendingPoolClearings(
|
|
16477
|
+
first: 1
|
|
16478
|
+
orderBy: endBlockNumber
|
|
16479
|
+
orderDirection: desc
|
|
16480
|
+
where: {
|
|
16481
|
+
lendingPool: $pool
|
|
16482
|
+
isFinished: true
|
|
16483
|
+
endBlockNumber_lt: $blockLt
|
|
16484
|
+
}
|
|
16485
|
+
) {
|
|
16486
|
+
epochId
|
|
16487
|
+
endBlockNumber
|
|
16488
|
+
endTimestamp
|
|
16489
|
+
}
|
|
16490
|
+
}
|
|
16491
|
+
`;
|
|
16492
|
+
function fetchClearingBeforeBlock(request, poolId, blockLt, cancellation) {
|
|
16493
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
16494
|
+
cancellation.throwIfCancelled();
|
|
16495
|
+
const page = yield request(poolTvlClearingBeforeBlockQuery, { pool: poolId, blockLt: String(blockLt) }, cancellation.signal);
|
|
16496
|
+
const row = page.clearings.at(0);
|
|
16497
|
+
return row === undefined ? null : parseClearing(row);
|
|
16498
|
+
});
|
|
16499
|
+
}
|
|
16500
|
+
/**
|
|
16501
|
+
* The newest finished clearing numbered below `beforeEpoch`: the opening
|
|
16502
|
+
* candidate of a `fromEpoch` window, whose own fetch never reads that far
|
|
16503
|
+
* back. ONE extra request for one row, no snapshot.
|
|
16504
|
+
*/
|
|
16505
|
+
function fetchClearingBefore(request, poolId, beforeEpoch, cancellation) {
|
|
16506
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
16507
|
+
cancellation.throwIfCancelled();
|
|
16508
|
+
const page = yield request(poolTvlClearingsQuery, {
|
|
16509
|
+
poolId,
|
|
16510
|
+
pool: poolId,
|
|
16511
|
+
epochGte: '0',
|
|
16512
|
+
epochLt: String(beforeEpoch),
|
|
16513
|
+
direction: 'desc',
|
|
16514
|
+
first: 1,
|
|
16515
|
+
withSnapshot: false,
|
|
16516
|
+
}, cancellation.signal);
|
|
16517
|
+
const row = page.clearings.at(0);
|
|
16518
|
+
return row === undefined ? null : parseClearing(row);
|
|
16519
|
+
});
|
|
16520
|
+
}
|
|
16253
16521
|
/**
|
|
16254
16522
|
* The pool's balance (display units, the subgraph's decimal string) at the end
|
|
16255
16523
|
* of each block, `TVL_HISTORY_BLOCKS_PER_REQUEST` blocks per request. A block
|
|
@@ -16653,6 +16921,42 @@ class DataService {
|
|
|
16653
16921
|
*/
|
|
16654
16922
|
getPoolTvlHistory(poolId_1) {
|
|
16655
16923
|
return __awaiter(this, arguments, void 0, function* (poolId, options = {}) {
|
|
16924
|
+
return (yield this._poolTvlHistory(poolId, options, false)).entries;
|
|
16925
|
+
});
|
|
16926
|
+
}
|
|
16927
|
+
/**
|
|
16928
|
+
* `getPoolTvlHistory` — the same options, errors and `entries` — plus
|
|
16929
|
+
* `opening`: the pool's value just before the window, so a strategy
|
|
16930
|
+
* summed across deployments (`combineTvlHistories`) knows what each leg
|
|
16931
|
+
* held before its first point instead of counting it 0.
|
|
16932
|
+
*
|
|
16933
|
+
* `opening` is the newest finished clearing STRICTLY before the window's
|
|
16934
|
+
* first block (by `pickOpeningClearing`), read at its own block exactly
|
|
16935
|
+
* like every other point and through the same past-block cache; `null`
|
|
16936
|
+
* when the pool has no clearing before the window. Extra cost over
|
|
16937
|
+
* `getPoolTvlHistory`:
|
|
16938
|
+
*
|
|
16939
|
+
* - `count` only: one more clearing row in the same request (`count + 1`,
|
|
16940
|
+
* which always reaches past the window when older history exists);
|
|
16941
|
+
* - `fromEpoch` above 0: one more one-row subgraph request, in parallel
|
|
16942
|
+
* with the clearings;
|
|
16943
|
+
* - only when every left-out clearing finished in the window's first
|
|
16944
|
+
* block (a catch-up block that closed several epochs at once): one
|
|
16945
|
+
* more one-row request by block (`fetchClearingBeforeBlock`), after
|
|
16946
|
+
* the clearings — the common case never makes it;
|
|
16947
|
+
* - either way, the opening's block in the same batched time-travel read
|
|
16948
|
+
* and one more `externalTVLOfPool` read (cached once final).
|
|
16949
|
+
*
|
|
16950
|
+
* `{ entries: [], opening: null }` wherever `getPoolTvlHistory` returns
|
|
16951
|
+
* `[]`.
|
|
16952
|
+
*/
|
|
16953
|
+
getPoolTvlHistoryWindow(poolId_1) {
|
|
16954
|
+
return __awaiter(this, arguments, void 0, function* (poolId, options = {}) {
|
|
16955
|
+
return yield this._poolTvlHistory(poolId, options, true);
|
|
16956
|
+
});
|
|
16957
|
+
}
|
|
16958
|
+
_poolTvlHistory(poolId, options, withOpening) {
|
|
16959
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
16656
16960
|
var _a;
|
|
16657
16961
|
validatePoolTvlHistoryOptions(options);
|
|
16658
16962
|
if (!utils$2.isAddress(poolId)) {
|
|
@@ -16671,33 +16975,56 @@ class DataService {
|
|
|
16671
16975
|
rpcs,
|
|
16672
16976
|
cancellation,
|
|
16673
16977
|
})
|
|
16674
|
-
: null, externalTvl !== null && externalTvl !== void 0 ? externalTvl : '', cancellation));
|
|
16978
|
+
: null, externalTvl !== null && externalTvl !== void 0 ? externalTvl : '', cancellation, withOpening));
|
|
16675
16979
|
});
|
|
16676
16980
|
}
|
|
16677
|
-
_readPoolTvlHistory(pool, options, reader, externalTvl, cancellation) {
|
|
16981
|
+
_readPoolTvlHistory(pool, options, reader, externalTvl, cancellation, withOpening) {
|
|
16678
16982
|
return __awaiter(this, void 0, void 0, function* () {
|
|
16679
16983
|
var _a, _b, _c;
|
|
16984
|
+
const nothing = { entries: [], opening: null };
|
|
16680
16985
|
const graph = (document, variables, signal) => this._graph.request({ document, variables, signal });
|
|
16681
16986
|
const scope = options.fromEpoch === undefined && options.count !== undefined
|
|
16682
|
-
? {
|
|
16987
|
+
? {
|
|
16988
|
+
kind: 'newest',
|
|
16989
|
+
// One past the window: the opening, when there is one.
|
|
16990
|
+
count: options.count + (withOpening ? 1 : 0),
|
|
16991
|
+
}
|
|
16683
16992
|
: { kind: 'from', fromEpoch: (_a = options.fromEpoch) !== null && _a !== void 0 ? _a : 0 };
|
|
16684
16993
|
try {
|
|
16685
|
-
const [currentEpochNumber, { clearings, snapshot }] = yield Promise.all([
|
|
16994
|
+
const [currentEpochNumber, { clearings, snapshot }, before] = yield Promise.all([
|
|
16686
16995
|
this._systemVariablesAbi.currentEpochNumber(),
|
|
16687
16996
|
fetchPoolClearings(graph, pool, scope, cancellation),
|
|
16997
|
+
withOpening && scope.kind === 'from' && scope.fromEpoch > 0
|
|
16998
|
+
? fetchClearingBefore(graph, pool, scope.fromEpoch, cancellation)
|
|
16999
|
+
: Promise.resolve(null),
|
|
16688
17000
|
]);
|
|
16689
17001
|
const currentEpoch = currentEpochNumber.toNumber();
|
|
16690
17002
|
const window = resolvePoolTvlHistoryWindow(options, currentEpoch);
|
|
16691
17003
|
const poolNow = snapshot.pool;
|
|
16692
17004
|
if (!window || !poolNow)
|
|
16693
|
-
return
|
|
17005
|
+
return nothing;
|
|
16694
17006
|
const head = (_b = snapshot.meta) === null || _b === void 0 ? void 0 : _b.block;
|
|
16695
17007
|
if (!head || typeof head.timestamp !== 'number') {
|
|
16696
17008
|
throw new Error('getPoolTvlHistory: the subgraph returned no latest block time');
|
|
16697
17009
|
}
|
|
16698
17010
|
const inWindow = clearings.filter((c) => c.epochId >= window.fromEpoch &&
|
|
16699
17011
|
c.epochId <= window.toEpoch);
|
|
16700
|
-
|
|
17012
|
+
// Clearings the window left out, older by epoch: the extra row of
|
|
17013
|
+
// a count fetch, or the one-row read below `fromEpoch`.
|
|
17014
|
+
const older = scope.kind === 'newest'
|
|
17015
|
+
? clearings.filter((c) => c.epochId < window.fromEpoch)
|
|
17016
|
+
: before === null
|
|
17017
|
+
? []
|
|
17018
|
+
: [before];
|
|
17019
|
+
let opening = withOpening
|
|
17020
|
+
? pickOpeningClearing(older, inWindow)
|
|
17021
|
+
: null;
|
|
17022
|
+
// The left-out clearings all sat in the first in-window block (a
|
|
17023
|
+
// catch-up block): ask for the newest one before it, by block.
|
|
17024
|
+
if (withOpening && needsClearingBeforeBlock(older, opening)) {
|
|
17025
|
+
opening = yield fetchClearingBeforeBlock(graph, pool, Math.min(...inWindow.map((c) => c.block)), cancellation);
|
|
17026
|
+
}
|
|
17027
|
+
const blocks = distinctBlocks(opening ? [...inWindow, opening] : inWindow);
|
|
16701
17028
|
const withCurrent = window.toEpoch >= currentEpoch;
|
|
16702
17029
|
const cacheable = (block) => block <= head.number - TVL_HISTORY_CACHE_DEPTH;
|
|
16703
17030
|
const [balances, offChainRead] = yield Promise.all([
|
|
@@ -16715,7 +17042,7 @@ class DataService {
|
|
|
16715
17042
|
latest: null,
|
|
16716
17043
|
},
|
|
16717
17044
|
]);
|
|
16718
|
-
|
|
17045
|
+
const entries = buildPoolTvlHistory({
|
|
16719
17046
|
clearings: inWindow,
|
|
16720
17047
|
balances,
|
|
16721
17048
|
offChain: offChainRead.offChain,
|
|
@@ -16729,10 +17056,22 @@ class DataService {
|
|
|
16729
17056
|
: null,
|
|
16730
17057
|
decimals: this._kasuConfig.stableAssetDecimals,
|
|
16731
17058
|
});
|
|
17059
|
+
// The same function as every other point, so the opening cannot
|
|
17060
|
+
// be computed another way.
|
|
17061
|
+
const [openingEntry] = opening
|
|
17062
|
+
? buildPoolTvlHistory({
|
|
17063
|
+
clearings: [opening],
|
|
17064
|
+
balances,
|
|
17065
|
+
offChain: offChainRead.offChain,
|
|
17066
|
+
current: null,
|
|
17067
|
+
decimals: this._kasuConfig.stableAssetDecimals,
|
|
17068
|
+
})
|
|
17069
|
+
: [null];
|
|
17070
|
+
return { entries, opening: openingEntry };
|
|
16732
17071
|
}
|
|
16733
17072
|
catch (error) {
|
|
16734
17073
|
if (isEmptyHistoryError(error, this._kasuConfig.isLiteDeployment)) {
|
|
16735
|
-
return
|
|
17074
|
+
return nothing;
|
|
16736
17075
|
}
|
|
16737
17076
|
throw error;
|
|
16738
17077
|
}
|
|
@@ -25699,9 +26038,9 @@ class SponsoredCallInputError extends Error {
|
|
|
25699
26038
|
Object.setPrototypeOf(this, SponsoredCallInputError.prototype);
|
|
25700
26039
|
}
|
|
25701
26040
|
}
|
|
25702
|
-
const ADDRESS_RE$
|
|
26041
|
+
const ADDRESS_RE$3 = /^0x[0-9a-fA-F]{40}$/;
|
|
25703
26042
|
function assertAddress(value, code) {
|
|
25704
|
-
if (typeof value !== 'string' || !ADDRESS_RE$
|
|
26043
|
+
if (typeof value !== 'string' || !ADDRESS_RE$3.test(value)) {
|
|
25705
26044
|
throw new SponsoredCallInputError(code);
|
|
25706
26045
|
}
|
|
25707
26046
|
}
|
|
@@ -25830,7 +26169,7 @@ function encodeStableTransfer(input) {
|
|
|
25830
26169
|
};
|
|
25831
26170
|
}
|
|
25832
26171
|
|
|
25833
|
-
const ADDRESS_RE$
|
|
26172
|
+
const ADDRESS_RE$2 = /^0x[0-9a-fA-F]{40}$/;
|
|
25834
26173
|
/**
|
|
25835
26174
|
* Encoded, ready-to-sponsor `LendingPoolManager` calls — read, then encode,
|
|
25836
26175
|
* NEVER send. Each returns `{ kind, call }`: post `kind` and `[call]` to a
|
|
@@ -25867,13 +26206,13 @@ class CallsFacade {
|
|
|
25867
26206
|
requestWithdrawal(params) {
|
|
25868
26207
|
return __awaiter(this, void 0, void 0, function* () {
|
|
25869
26208
|
const { poolId, trancheId, amount, userAddress } = params;
|
|
25870
|
-
if (typeof userAddress !== 'string' || !ADDRESS_RE$
|
|
26209
|
+
if (typeof userAddress !== 'string' || !ADDRESS_RE$2.test(userAddress)) {
|
|
25871
26210
|
throw new SponsoredCallInputError('user');
|
|
25872
26211
|
}
|
|
25873
|
-
if (typeof poolId !== 'string' || !ADDRESS_RE$
|
|
26212
|
+
if (typeof poolId !== 'string' || !ADDRESS_RE$2.test(poolId)) {
|
|
25874
26213
|
throw new SponsoredCallInputError('pool');
|
|
25875
26214
|
}
|
|
25876
|
-
if (typeof trancheId !== 'string' || !ADDRESS_RE$
|
|
26215
|
+
if (typeof trancheId !== 'string' || !ADDRESS_RE$2.test(trancheId)) {
|
|
25877
26216
|
throw new SponsoredCallInputError('tranche');
|
|
25878
26217
|
}
|
|
25879
26218
|
let shares;
|
|
@@ -26525,7 +26864,7 @@ const BEFORE_EXECUTION = ENTRY_POINT_EVENTS.getEventTopic('BeforeExecution');
|
|
|
26525
26864
|
const DEPOSIT_REQUESTED = PENDING_POOL_EVENTS.getEventTopic('DepositRequested');
|
|
26526
26865
|
const ERC20$1 = IERC20MetadataAbi__factory.createInterface();
|
|
26527
26866
|
const LENDING_POOL_MANAGER$1 = ILendingPoolManagerAbi__factory.createInterface();
|
|
26528
|
-
const ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
|
|
26867
|
+
const ADDRESS_RE$1 = /^0x[0-9a-fA-F]{40}$/;
|
|
26529
26868
|
/**
|
|
26530
26869
|
* Case-insensitive address equality; `false` for anything that is not a
|
|
26531
26870
|
* 20-byte hex address. Checksums are not enforced — an all-lowercase address
|
|
@@ -26534,8 +26873,8 @@ const ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
|
|
|
26534
26873
|
function sameAddress(a, b) {
|
|
26535
26874
|
return (typeof a === 'string' &&
|
|
26536
26875
|
typeof b === 'string' &&
|
|
26537
|
-
ADDRESS_RE.test(a) &&
|
|
26538
|
-
ADDRESS_RE.test(b) &&
|
|
26876
|
+
ADDRESS_RE$1.test(a) &&
|
|
26877
|
+
ADDRESS_RE$1.test(b) &&
|
|
26539
26878
|
a.toLowerCase() === b.toLowerCase());
|
|
26540
26879
|
}
|
|
26541
26880
|
function isHexData(value) {
|
|
@@ -26561,7 +26900,7 @@ function isCallShape(call) {
|
|
|
26561
26900
|
if (call === null || typeof call !== 'object')
|
|
26562
26901
|
return false;
|
|
26563
26902
|
const { to, data } = call;
|
|
26564
|
-
return typeof to === 'string' && ADDRESS_RE.test(to) && isHexData(data);
|
|
26903
|
+
return typeof to === 'string' && ADDRESS_RE$1.test(to) && isHexData(data);
|
|
26565
26904
|
}
|
|
26566
26905
|
function isZeroValue(value) {
|
|
26567
26906
|
if (value === undefined)
|
|
@@ -27539,19 +27878,30 @@ function buildLoanAgreementSignMessage(p) {
|
|
|
27539
27878
|
].join('\n');
|
|
27540
27879
|
}
|
|
27541
27880
|
/**
|
|
27542
|
-
* The legacy `/contract/generate`
|
|
27881
|
+
* The legacy `/contract/generate` message.
|
|
27543
27882
|
*
|
|
27544
27883
|
* ⚠️ BYTE-EXACT PROTOCOL STRING. kasu-backend rebuilds it as
|
|
27545
27884
|
* `` `I request contract content for ${address} at ${timestamp}.` `` from the
|
|
27546
27885
|
* `address` and `timestamp` fields of the request body — so the string signed
|
|
27547
27886
|
* and the body sent must agree exactly, INCLUDING the address casing. This
|
|
27548
27887
|
* builder lowercases, and the request body must carry the same lowercased
|
|
27549
|
-
* address
|
|
27888
|
+
* address.
|
|
27550
27889
|
*
|
|
27551
27890
|
* The backend takes this path whenever the four human-readable display fields
|
|
27552
|
-
* are absent
|
|
27553
|
-
*
|
|
27554
|
-
* signs
|
|
27891
|
+
* are absent. Only the retired kasu-fe-next app (retired 2026-11-05) and
|
|
27892
|
+
* kasu-mobile builds from before 2026-09-06 signed it; every current consumer
|
|
27893
|
+
* signs `buildLoanAgreementSignMessage`. The backend keeps accepting it until
|
|
27894
|
+
* no such mobile build is left in use.
|
|
27895
|
+
*
|
|
27896
|
+
* It is NOT the `/contract/resolve` message. Resolve has a message of its own
|
|
27897
|
+
* (`Requesting contract content for {address}/{id} at {timestamp}.`), which
|
|
27898
|
+
* kasu-backend rebuilds and verifies, so it does not depend on this format.
|
|
27899
|
+
*
|
|
27900
|
+
* @deprecated Sign the human-readable loan-agreement message
|
|
27901
|
+
* (`buildLoanAgreementSignMessage`, `contractMessage.format:
|
|
27902
|
+
* 'loan-agreement'`) instead. Only the retired kasu-fe-next app and
|
|
27903
|
+
* pre-2026-09-06 kasu-mobile builds used this format; it will be removed in
|
|
27904
|
+
* the next major version.
|
|
27555
27905
|
*
|
|
27556
27906
|
* @param timestampMs ms-epoch, and the same value sent as the body's
|
|
27557
27907
|
* `timestamp`.
|
|
@@ -28970,6 +29320,381 @@ function maxNetRateCeiling(pools, feePercent) {
|
|
|
28970
29320
|
return rate;
|
|
28971
29321
|
}
|
|
28972
29322
|
|
|
29323
|
+
/**
|
|
29324
|
+
* The CMS-authored figures and text a strategy's Details screen shows, read
|
|
29325
|
+
* the way kasu-ui's Details tab reads them, so every app shows the same
|
|
29326
|
+
* thing.
|
|
29327
|
+
*
|
|
29328
|
+
* Lifted from kasu-ui (`lib/details-fields.ts`, `lib/directus-text.ts` and
|
|
29329
|
+
* the parsers inside `deposit-dialog/details-tab.tsx`). The rules are facts
|
|
29330
|
+
* about how the CMS is authored, not presentation:
|
|
29331
|
+
*
|
|
29332
|
+
* - EMPTY IS UNAUTHORED. The CMS stores an emptied field as `''`, and its
|
|
29333
|
+
* "numeric" fields arrive as a number, a numeric string, `''` or `null`.
|
|
29334
|
+
* Anything that is not real data reads as `undefined` — never a `0`, a
|
|
29335
|
+
* placeholder or a sentence of the SDK's own. A field the caller cannot
|
|
29336
|
+
* read is a row the caller omits.
|
|
29337
|
+
* - A START DATE MUST DATE. A value that is not an ISO date, or a real
|
|
29338
|
+
* calendar date, or is before 2000 or in the future, is refused: rendered
|
|
29339
|
+
* as elapsed time it would claim a history nobody authored.
|
|
29340
|
+
* - SOME FIELDS CARRY TWO LINES IN ONE STRING. Business Types and Facility
|
|
29341
|
+
* Type are a primary value and a qualifying clause; risk-item
|
|
29342
|
+
* descriptions can be numbered enumerations. Splitting them is the same
|
|
29343
|
+
* everywhere, so it lives here.
|
|
29344
|
+
*
|
|
29345
|
+
* Like `pickShortDescription`, this authors no string of its own: it selects,
|
|
29346
|
+
* splits and trims text the CMS already holds. None of it is sanitised —
|
|
29347
|
+
* render every string as text, never as HTML or markdown.
|
|
29348
|
+
*
|
|
29349
|
+
* Pure. No clock: the date reader takes `now` from its caller.
|
|
29350
|
+
*/
|
|
29351
|
+
// ---------------------------------------------------------------------------
|
|
29352
|
+
// Start dates
|
|
29353
|
+
// ---------------------------------------------------------------------------
|
|
29354
|
+
/**
|
|
29355
|
+
* `YYYY-MM-DD`, optionally followed by a time part — the two shapes a CMS
|
|
29356
|
+
* date column serialises to. Checked BEFORE any parse: `new Date('Jun 2024')`
|
|
29357
|
+
* is a valid date in V8, so an editorial string would otherwise become a
|
|
29358
|
+
* confident duration.
|
|
29359
|
+
*/
|
|
29360
|
+
const ISO_DATE = /^(\d{4})-(\d{2})-(\d{2})(?:[T ].*)?$/;
|
|
29361
|
+
/** Nothing on Kasu has a lending history from before this year. */
|
|
29362
|
+
const EARLIEST_YEAR = 2000;
|
|
29363
|
+
/**
|
|
29364
|
+
* A CMS start date as its `YYYY-MM-DD` date head, or `undefined`.
|
|
29365
|
+
*
|
|
29366
|
+
* Accepts an ISO date, optionally with a time part (only the date head is
|
|
29367
|
+
* kept — a time changes nothing at year/month granularity, and converting it
|
|
29368
|
+
* through a timezone could move it a day). Refused, as `undefined`:
|
|
29369
|
+
*
|
|
29370
|
+
* - anything that is not a string (or, with `unixSeconds`, a number);
|
|
29371
|
+
* - an empty or blank string (unauthored);
|
|
29372
|
+
* - a string that is not ISO-shaped (`'Jun 2024'`);
|
|
29373
|
+
* - a date that does not exist (`'2024-02-30'`, `'2024-13-01'`) — never
|
|
29374
|
+
* rolled over into the next month;
|
|
29375
|
+
* - a year before 2000;
|
|
29376
|
+
* - a date after `now` (a typo such as 2062 for 2026 — no history has
|
|
29377
|
+
* begun in the future).
|
|
29378
|
+
*
|
|
29379
|
+
* `now` is the caller's clock; the facade passes the current time.
|
|
29380
|
+
*/
|
|
29381
|
+
function readCmsStartDate(value, now, options = {}) {
|
|
29382
|
+
let head;
|
|
29383
|
+
if (typeof value === 'number' && options.unixSeconds) {
|
|
29384
|
+
// `0` is formatDuration's "unset" sentinel; negatives are pre-1970.
|
|
29385
|
+
if (!Number.isFinite(value) || value <= 0)
|
|
29386
|
+
return undefined;
|
|
29387
|
+
const date = new Date(value * 1000);
|
|
29388
|
+
if (Number.isNaN(date.getTime()))
|
|
29389
|
+
return undefined;
|
|
29390
|
+
head = date.toISOString().slice(0, 10);
|
|
29391
|
+
}
|
|
29392
|
+
else if (typeof value === 'string') {
|
|
29393
|
+
const trimmed = value.trim();
|
|
29394
|
+
if (!ISO_DATE.test(trimmed))
|
|
29395
|
+
return undefined;
|
|
29396
|
+
head = trimmed.slice(0, 10);
|
|
29397
|
+
}
|
|
29398
|
+
else {
|
|
29399
|
+
return undefined;
|
|
29400
|
+
}
|
|
29401
|
+
const match = ISO_DATE.exec(head);
|
|
29402
|
+
if (!match)
|
|
29403
|
+
return undefined;
|
|
29404
|
+
const year = Number(match[1]);
|
|
29405
|
+
const month = Number(match[2]);
|
|
29406
|
+
const day = Number(match[3]);
|
|
29407
|
+
// Round-trip through UTC: a day or month out of range comes back as a
|
|
29408
|
+
// different date, which is how a non-existent date is caught.
|
|
29409
|
+
const utc = new Date(Date.UTC(year, month - 1, day));
|
|
29410
|
+
if (utc.getUTCFullYear() !== year ||
|
|
29411
|
+
utc.getUTCMonth() !== month - 1 ||
|
|
29412
|
+
utc.getUTCDate() !== day) {
|
|
29413
|
+
return undefined;
|
|
29414
|
+
}
|
|
29415
|
+
if (year < EARLIEST_YEAR)
|
|
29416
|
+
return undefined;
|
|
29417
|
+
if (utc.getTime() > now.getTime())
|
|
29418
|
+
return undefined;
|
|
29419
|
+
return head;
|
|
29420
|
+
}
|
|
29421
|
+
/**
|
|
29422
|
+
* Business Types (the CMS `industryExposure`), split into the value and its
|
|
29423
|
+
* supporting clause on the FIRST newline, else the FIRST parenthesised
|
|
29424
|
+
* segment (a trailing `)` dropped):
|
|
29425
|
+
*
|
|
29426
|
+
* `'SMEs & Large Businesses (diversified industries – exclusions apply)'`
|
|
29427
|
+
* → `{ primary: 'SMEs & Large Businesses', detail: 'diversified industries – exclusions apply' }`
|
|
29428
|
+
*
|
|
29429
|
+
* A value with neither delimiter is `{ primary }` alone. `undefined` for a
|
|
29430
|
+
* missing, non-string, empty or blank value.
|
|
29431
|
+
*
|
|
29432
|
+
* REFINED over kasu-ui: a value that OPENS with `(` would split to an empty
|
|
29433
|
+
* primary there; here the whole value is the primary instead, so `primary`
|
|
29434
|
+
* is never empty. No live record has that shape.
|
|
29435
|
+
*/
|
|
29436
|
+
function splitBusinessTypes(raw) {
|
|
29437
|
+
const value = typeof raw === 'string' ? raw.trim() : '';
|
|
29438
|
+
if (!value)
|
|
29439
|
+
return undefined;
|
|
29440
|
+
let primary = value;
|
|
29441
|
+
let detail = '';
|
|
29442
|
+
const newlineAt = value.indexOf('\n');
|
|
29443
|
+
if (newlineAt !== -1) {
|
|
29444
|
+
primary = value.slice(0, newlineAt).trim();
|
|
29445
|
+
detail = value.slice(newlineAt + 1).trim();
|
|
29446
|
+
}
|
|
29447
|
+
else {
|
|
29448
|
+
const parenAt = value.indexOf('(');
|
|
29449
|
+
if (parenAt !== -1) {
|
|
29450
|
+
primary = value.slice(0, parenAt).trim();
|
|
29451
|
+
detail = value.slice(parenAt + 1).trim();
|
|
29452
|
+
if (detail.endsWith(')'))
|
|
29453
|
+
detail = detail.slice(0, -1).trim();
|
|
29454
|
+
}
|
|
29455
|
+
}
|
|
29456
|
+
if (!primary)
|
|
29457
|
+
return { primary: value };
|
|
29458
|
+
return detail ? { primary, detail } : { primary };
|
|
29459
|
+
}
|
|
29460
|
+
/**
|
|
29461
|
+
* The start of the label, or a character that is neither a word character
|
|
29462
|
+
* nor a hyphen, before `revolv` — not `\b`, which treats a hyphen as a
|
|
29463
|
+
* boundary, so "Non-Revolving" would match. kasu-ui writes the same rule as
|
|
29464
|
+
* the lookbehind `(?<![\w-])`; this form needs no lookbehind support from the
|
|
29465
|
+
* JavaScript engine.
|
|
29466
|
+
*/
|
|
29467
|
+
const REVOLVING_FACILITY = /(?:^|[^\w-])revolv\w*\b/i;
|
|
29468
|
+
/**
|
|
29469
|
+
* Facility Type (the CMS `loanStructure`): the label on the FIRST line, and
|
|
29470
|
+
* everything after the first newline as its clause, so a clause with line
|
|
29471
|
+
* breaks of its own stays whole.
|
|
29472
|
+
*
|
|
29473
|
+
* `'Revolving Instalment Facility\n12-24 months instalment loan, subject to annual review'`
|
|
29474
|
+
* → `{ label: 'Revolving Instalment Facility', detail: '12-24 months …', revolving: true }`
|
|
29475
|
+
*
|
|
29476
|
+
* `undefined` for a missing, non-string, empty or blank value. The value is
|
|
29477
|
+
* trimmed first, so a leading blank line still yields a label.
|
|
29478
|
+
*/
|
|
29479
|
+
function parseFacility(raw) {
|
|
29480
|
+
const value = typeof raw === 'string' ? raw.trim() : '';
|
|
29481
|
+
if (!value)
|
|
29482
|
+
return undefined;
|
|
29483
|
+
const breakAt = value.indexOf('\n');
|
|
29484
|
+
const label = (breakAt === -1 ? value : value.slice(0, breakAt)).trim();
|
|
29485
|
+
const detail = breakAt === -1 ? '' : value.slice(breakAt + 1).trim();
|
|
29486
|
+
const revolving = REVOLVING_FACILITY.test(label);
|
|
29487
|
+
return detail ? { label, detail, revolving } : { label, revolving };
|
|
29488
|
+
}
|
|
29489
|
+
// ---------------------------------------------------------------------------
|
|
29490
|
+
// Risk Management
|
|
29491
|
+
// ---------------------------------------------------------------------------
|
|
29492
|
+
/**
|
|
29493
|
+
* The order the Risk Management groups read in, by group name (compared
|
|
29494
|
+
* trimmed, case-insensitively). The CMS returns items in authoring order,
|
|
29495
|
+
* which is not this. A group not listed still appears — after these, in CMS
|
|
29496
|
+
* order.
|
|
29497
|
+
*/
|
|
29498
|
+
const RISK_GROUP_ORDER = Object.freeze([
|
|
29499
|
+
'Security & Recourse',
|
|
29500
|
+
'Structural Controls',
|
|
29501
|
+
'Financial Covenants',
|
|
29502
|
+
'Reporting Covenants',
|
|
29503
|
+
'Key Eligibility Criteria',
|
|
29504
|
+
]);
|
|
29505
|
+
/** Zero-width paste artifacts, which `\s` does not match. */
|
|
29506
|
+
const ZERO_WIDTH = /[-]/g;
|
|
29507
|
+
/** An `N)` marker at the start, or after a `;` / `; and,` separator. */
|
|
29508
|
+
const HAS_ENUMERATION = /(?:^\d+\)|;\s*(?:and,?\s*)?\d+\))/;
|
|
29509
|
+
const ENUMERATION_SPLIT = /(?:^|\s*;\s*(?:and,?\s*)?)\d+\)\s*/;
|
|
29510
|
+
/** kasu-ui's stub for an unauthored description; never a clause. */
|
|
29511
|
+
const EMPTY_DESCRIPTION = '—';
|
|
29512
|
+
/**
|
|
29513
|
+
* A risk-item description as the lines beneath the item's name.
|
|
29514
|
+
*
|
|
29515
|
+
* A numbered enumeration (`'1) Foo; and, 2) Bar'`, or one that omits its
|
|
29516
|
+
* leading `1)`: `'Foo; 2) Bar'`) is one line per clause; anything else is
|
|
29517
|
+
* one line. Zero-width characters are stripped first. An unauthored
|
|
29518
|
+
* description is NO lines (`[]`), never a dash.
|
|
29519
|
+
*/
|
|
29520
|
+
function splitNumberedClauses(description) {
|
|
29521
|
+
const raw = cleanDescription(description);
|
|
29522
|
+
if (!raw)
|
|
29523
|
+
return [];
|
|
29524
|
+
const clauses = HAS_ENUMERATION.test(raw)
|
|
29525
|
+
? raw
|
|
29526
|
+
.split(ENUMERATION_SPLIT)
|
|
29527
|
+
.map((part) => part.trim())
|
|
29528
|
+
.filter((part) => part.length > 0)
|
|
29529
|
+
: [raw];
|
|
29530
|
+
const lines = clauses.length > 0 ? clauses : [raw];
|
|
29531
|
+
return lines.filter((line) => line !== EMPTY_DESCRIPTION);
|
|
29532
|
+
}
|
|
29533
|
+
/** A description with zero-width characters stripped, trimmed; `''` when not a string. */
|
|
29534
|
+
function cleanDescription(description) {
|
|
29535
|
+
return typeof description === 'string'
|
|
29536
|
+
? description.replace(ZERO_WIDTH, '').trim()
|
|
29537
|
+
: '';
|
|
29538
|
+
}
|
|
29539
|
+
/**
|
|
29540
|
+
* Risk-management items grouped by their group, the groups in
|
|
29541
|
+
* `RISK_GROUP_ORDER` and, within a group, the items in CMS order.
|
|
29542
|
+
*
|
|
29543
|
+
* An item with no name, or no group, is left out: there is nothing to head
|
|
29544
|
+
* its lines with, or no group to place it in. (The CMS joins items to groups
|
|
29545
|
+
* by name, so neither occurs in live data.) No group is ever empty, so an
|
|
29546
|
+
* item list with nothing usable gives `[]`.
|
|
29547
|
+
*/
|
|
29548
|
+
function groupRiskManagementItems(items) {
|
|
29549
|
+
const groups = new Map();
|
|
29550
|
+
for (const source of items) {
|
|
29551
|
+
const name = trimmedOrUndefined(source.name);
|
|
29552
|
+
const group = trimmedOrUndefined(source.group);
|
|
29553
|
+
if (!name || !group)
|
|
29554
|
+
continue;
|
|
29555
|
+
const clauses = splitNumberedClauses(source.description);
|
|
29556
|
+
// The same cleaned text the clauses come from, and only when they
|
|
29557
|
+
// say something: a description of "—" or of zero-width characters
|
|
29558
|
+
// alone is unauthored, so it is absent here too, never a stray dash.
|
|
29559
|
+
const description = clauses.length > 0 ? cleanDescription(source.description) : '';
|
|
29560
|
+
const tooltip = trimmedOrUndefined(source.tooltip);
|
|
29561
|
+
const item = Object.assign(Object.assign(Object.assign({ name }, (description ? { description } : {})), { clauses }), (tooltip ? { tooltip } : {}));
|
|
29562
|
+
const existing = groups.get(group);
|
|
29563
|
+
if (existing)
|
|
29564
|
+
existing.items.push(item);
|
|
29565
|
+
else
|
|
29566
|
+
groups.set(group, { group, items: [item] });
|
|
29567
|
+
}
|
|
29568
|
+
// Array.prototype.sort is stable, so unlisted groups keep CMS order.
|
|
29569
|
+
return [...groups.values()].sort((a, b) => riskGroupRank(a.group) - riskGroupRank(b.group));
|
|
29570
|
+
}
|
|
29571
|
+
const RISK_GROUP_RANK = new Map(RISK_GROUP_ORDER.map((group, index) => [group.toLowerCase(), index]));
|
|
29572
|
+
function riskGroupRank(group) {
|
|
29573
|
+
var _a;
|
|
29574
|
+
return ((_a = RISK_GROUP_RANK.get(group.trim().toLowerCase())) !== null && _a !== void 0 ? _a : RISK_GROUP_ORDER.length);
|
|
29575
|
+
}
|
|
29576
|
+
/**
|
|
29577
|
+
* The pool's security tags (the CMS `security` list), trimmed, blanks
|
|
29578
|
+
* dropped. `undefined` when none remain.
|
|
29579
|
+
*
|
|
29580
|
+
* kasu-ui shows these under "Security Structure over End Borrowers" ONLY
|
|
29581
|
+
* when the pool has no risk-management items.
|
|
29582
|
+
*/
|
|
29583
|
+
function securityStructureOf(raw) {
|
|
29584
|
+
if (!Array.isArray(raw))
|
|
29585
|
+
return undefined;
|
|
29586
|
+
const tags = raw
|
|
29587
|
+
.map((tag) => trimmedOrUndefined(tag))
|
|
29588
|
+
.filter((tag) => tag !== undefined);
|
|
29589
|
+
return tags.length > 0 ? tags : undefined;
|
|
29590
|
+
}
|
|
29591
|
+
// ---------------------------------------------------------------------------
|
|
29592
|
+
// Numbers
|
|
29593
|
+
// ---------------------------------------------------------------------------
|
|
29594
|
+
/**
|
|
29595
|
+
* A CMS or subgraph "numeric" value as a finite number, or `undefined`. An
|
|
29596
|
+
* empty or blank string is UNAUTHORED, not zero; a genuine `0` survives.
|
|
29597
|
+
*/
|
|
29598
|
+
function finiteOrUndefined(value) {
|
|
29599
|
+
if (typeof value === 'number') {
|
|
29600
|
+
return Number.isFinite(value) ? value : undefined;
|
|
29601
|
+
}
|
|
29602
|
+
if (typeof value === 'string') {
|
|
29603
|
+
if (value.trim() === '')
|
|
29604
|
+
return undefined;
|
|
29605
|
+
const numeric = Number(value);
|
|
29606
|
+
return Number.isFinite(numeric) ? numeric : undefined;
|
|
29607
|
+
}
|
|
29608
|
+
return undefined;
|
|
29609
|
+
}
|
|
29610
|
+
/**
|
|
29611
|
+
* A leg `strategyTotalsUsd` cannot place: two legs for one deployment
|
|
29612
|
+
* (`duplicate-leg`, which would double-count it), or a leg for a deployment
|
|
29613
|
+
* not in `expectedDeployments` (`unexpected-leg`, a pool of some other
|
|
29614
|
+
* strategy). Thrown, because both are caller bugs, not data states.
|
|
29615
|
+
*/
|
|
29616
|
+
class StrategyTotalsInputError extends Error {
|
|
29617
|
+
constructor(code, deployment) {
|
|
29618
|
+
super(code === 'duplicate-leg'
|
|
29619
|
+
? `strategyTotalsUsd: two legs for deployment "${deployment}"`
|
|
29620
|
+
: `strategyTotalsUsd: leg for deployment "${deployment}" is not in expectedDeployments`);
|
|
29621
|
+
this.code = code;
|
|
29622
|
+
this.deployment = deployment;
|
|
29623
|
+
this.name = 'StrategyTotalsInputError';
|
|
29624
|
+
}
|
|
29625
|
+
}
|
|
29626
|
+
/**
|
|
29627
|
+
* Total Active Lending and Lender Interest Earned for a strategy across the
|
|
29628
|
+
* deployments it lends on, in USD: each leg's native amount times that leg's
|
|
29629
|
+
* own `usdRate`, summed. Tax Pay lends on Base (USDC) and XDC (AUDD);
|
|
29630
|
+
* 2,567,901 AUDD is not US$2,567,901.
|
|
29631
|
+
*
|
|
29632
|
+
* `expectedDeployments` is every deployment the strategy has a pool on —
|
|
29633
|
+
* `strategyDeployments(poolId, chain).map((d) => d.chain)` — and `legs` the
|
|
29634
|
+
* ones whose pool the caller actually loaded. What is guaranteed:
|
|
29635
|
+
*
|
|
29636
|
+
* - **`null` — WITHHELD** when there are no legs, or ANY leg's rate is
|
|
29637
|
+
* missing or unusable (not a finite number above 0). The total is never
|
|
29638
|
+
* built on a guessed rate, and never silently drops a loaded leg; the
|
|
29639
|
+
* caller shows the held pool's own figure, or nothing.
|
|
29640
|
+
* - **Otherwise the sum over `legs`**, with `isPartial` /
|
|
29641
|
+
* `missingDeployments` stating exactly which expected deployments it
|
|
29642
|
+
* does NOT cover. `isPartial: false` is the only case in which the
|
|
29643
|
+
* figures are the whole strategy.
|
|
29644
|
+
* - An amount that is absent or malformed contributes nothing (a real `0`
|
|
29645
|
+
* is still `0`).
|
|
29646
|
+
* - Throws `StrategyTotalsInputError` for a duplicated leg or one not in
|
|
29647
|
+
* `expectedDeployments`, before summing anything.
|
|
29648
|
+
*
|
|
29649
|
+
* Only these two figures are summed. The CMS-authored ones — every
|
|
29650
|
+
* originator figure, `operatingSince`, the loss rates — are shared by a
|
|
29651
|
+
* strategy's pools and already strategy-wide: summing them double-counts.
|
|
29652
|
+
*
|
|
29653
|
+
* Mirrors the money half of kasu-ui's `aggregateStrategyPools` and its
|
|
29654
|
+
* withhold gate.
|
|
29655
|
+
*/
|
|
29656
|
+
function strategyTotalsUsd(legs, expectedDeployments) {
|
|
29657
|
+
var _a, _b;
|
|
29658
|
+
const expected = new Set(expectedDeployments);
|
|
29659
|
+
const seen = new Set();
|
|
29660
|
+
for (const leg of legs) {
|
|
29661
|
+
if (seen.has(leg.deployment)) {
|
|
29662
|
+
throw new StrategyTotalsInputError('duplicate-leg', leg.deployment);
|
|
29663
|
+
}
|
|
29664
|
+
if (!expected.has(leg.deployment)) {
|
|
29665
|
+
throw new StrategyTotalsInputError('unexpected-leg', leg.deployment);
|
|
29666
|
+
}
|
|
29667
|
+
seen.add(leg.deployment);
|
|
29668
|
+
}
|
|
29669
|
+
if (legs.length === 0)
|
|
29670
|
+
return null;
|
|
29671
|
+
let totalActiveLendingUsd = 0;
|
|
29672
|
+
let lenderInterestEarnedUsd = 0;
|
|
29673
|
+
for (const leg of legs) {
|
|
29674
|
+
const rate = leg.usdRate;
|
|
29675
|
+
if (typeof rate !== 'number' || !Number.isFinite(rate) || rate <= 0) {
|
|
29676
|
+
return null;
|
|
29677
|
+
}
|
|
29678
|
+
totalActiveLendingUsd +=
|
|
29679
|
+
((_a = finiteOrUndefined(leg.totalActiveLending)) !== null && _a !== void 0 ? _a : 0) * rate;
|
|
29680
|
+
lenderInterestEarnedUsd +=
|
|
29681
|
+
((_b = finiteOrUndefined(leg.lenderInterestEarned)) !== null && _b !== void 0 ? _b : 0) * rate;
|
|
29682
|
+
}
|
|
29683
|
+
const missingDeployments = [...expected].filter((d) => !seen.has(d));
|
|
29684
|
+
return {
|
|
29685
|
+
totalActiveLendingUsd,
|
|
29686
|
+
lenderInterestEarnedUsd,
|
|
29687
|
+
isPartial: missingDeployments.length > 0,
|
|
29688
|
+
missingDeployments,
|
|
29689
|
+
};
|
|
29690
|
+
}
|
|
29691
|
+
function trimmedOrUndefined(value) {
|
|
29692
|
+
if (typeof value !== 'string')
|
|
29693
|
+
return undefined;
|
|
29694
|
+
const trimmed = value.trim();
|
|
29695
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
29696
|
+
}
|
|
29697
|
+
|
|
28973
29698
|
/**
|
|
28974
29699
|
* A strategy's SHORT blurb, as plain text: the CMS `liteDescription` first,
|
|
28975
29700
|
* else the first paragraph of the long `description`, else nothing.
|
|
@@ -29057,10 +29782,55 @@ class StrategiesFacade {
|
|
|
29057
29782
|
* Chain pool id → the pool id its CMS record is keyed by (XDC pools
|
|
29058
29783
|
* share their Base counterpart's editorial record). Lowercase keys.
|
|
29059
29784
|
*/
|
|
29060
|
-
_poolMetadataMapping = {}
|
|
29785
|
+
_poolMetadataMapping = {},
|
|
29786
|
+
/**
|
|
29787
|
+
* The clock the CMS start dates are checked against (a date after it
|
|
29788
|
+
* is refused). Injected for tests; the current time otherwise.
|
|
29789
|
+
*/
|
|
29790
|
+
_now = () => new Date(),
|
|
29791
|
+
/**
|
|
29792
|
+
* Milliseconds on a clock that only moves forward, for the shared
|
|
29793
|
+
* CMS reads' 30-second lifetime — separate from `_now`, so a pinned
|
|
29794
|
+
* date clock cannot keep a read alive for ever. Injected for tests;
|
|
29795
|
+
* `Date.now` otherwise.
|
|
29796
|
+
*/
|
|
29797
|
+
_memoClockMs = () => Date.now()) {
|
|
29061
29798
|
this._dataService = _dataService;
|
|
29062
29799
|
this._userLending = _userLending;
|
|
29063
29800
|
this._poolMetadataMapping = _poolMetadataMapping;
|
|
29801
|
+
this._now = _now;
|
|
29802
|
+
this._memoClockMs = _memoClockMs;
|
|
29803
|
+
/**
|
|
29804
|
+
* Short-lived, per-instance memo of the two CMS-wide reads the pool
|
|
29805
|
+
* getters share, so a Details screen asking for the loss rate, the risk
|
|
29806
|
+
* groups and the originator together reads each collection once. Keyed
|
|
29807
|
+
* by read; holds the in-flight promise, so concurrent callers share it.
|
|
29808
|
+
* A REJECTED read is dropped at once, never served again — the next call
|
|
29809
|
+
* retries.
|
|
29810
|
+
*/
|
|
29811
|
+
this._cmsReads = new Map();
|
|
29812
|
+
}
|
|
29813
|
+
_memoCmsRead(key, read) {
|
|
29814
|
+
const nowMs = this._memoClockMs();
|
|
29815
|
+
const cached = this._cmsReads.get(key);
|
|
29816
|
+
if (cached && cached.expiresAt > nowMs) {
|
|
29817
|
+
return cached.promise;
|
|
29818
|
+
}
|
|
29819
|
+
const promise = read();
|
|
29820
|
+
const entry = { promise, expiresAt: nowMs + CMS_READ_TTL_MS };
|
|
29821
|
+
this._cmsReads.set(key, entry);
|
|
29822
|
+
promise.catch(() => {
|
|
29823
|
+
// Only our own entry: a newer read may have replaced it.
|
|
29824
|
+
if (this._cmsReads.get(key) === entry)
|
|
29825
|
+
this._cmsReads.delete(key);
|
|
29826
|
+
});
|
|
29827
|
+
return promise;
|
|
29828
|
+
}
|
|
29829
|
+
_riskManagementRecords() {
|
|
29830
|
+
return this._memoCmsRead('riskManagement', () => this._dataService.getRiskManagement());
|
|
29831
|
+
}
|
|
29832
|
+
_originatorProfiles() {
|
|
29833
|
+
return this._memoCmsRead('originatorProfiles', () => this._dataService.getPoolDelegateProfileAndHistory());
|
|
29064
29834
|
}
|
|
29065
29835
|
/**
|
|
29066
29836
|
* The pool's loss rate and first-loss capital, from its CMS
|
|
@@ -29073,12 +29843,14 @@ class StrategiesFacade {
|
|
|
29073
29843
|
* 0..1 fraction.
|
|
29074
29844
|
*
|
|
29075
29845
|
* Requires `directusUrl`; without it the call rejects with
|
|
29076
|
-
* `NO_DIRECTUS_URL_MESSAGE`, like every CMS-only read.
|
|
29846
|
+
* `NO_DIRECTUS_URL_MESSAGE`, like every CMS-only read. Shares its CMS
|
|
29847
|
+
* read with `getRiskManagementGroups` for 30 seconds per instance; a
|
|
29848
|
+
* failed read is never reused.
|
|
29077
29849
|
*/
|
|
29078
29850
|
getRiskPerformance(poolId) {
|
|
29079
29851
|
return __awaiter(this, void 0, void 0, function* () {
|
|
29080
29852
|
const metadataId = this._metadataIdOf(poolId);
|
|
29081
|
-
const records = yield this.
|
|
29853
|
+
const records = yield this._riskManagementRecords();
|
|
29082
29854
|
const record = records.find((r) => String(r.poolIdFK).toLowerCase() === metadataId);
|
|
29083
29855
|
if (!record)
|
|
29084
29856
|
return null;
|
|
@@ -29102,16 +29874,95 @@ class StrategiesFacade {
|
|
|
29102
29874
|
* deployment every profile lists no pools and this resolves `null` —
|
|
29103
29875
|
* the same answer kasu-ui's own lookup gives there today. The metadata
|
|
29104
29876
|
* candidate becomes live once the service resolves the mapping.
|
|
29877
|
+
*
|
|
29878
|
+
* Shares its reads with `getOriginatorHistory` for 30 seconds per
|
|
29879
|
+
* instance; a failed read is never reused. Each call returns its own
|
|
29880
|
+
* copy of the profile, so mutating it changes nothing shared.
|
|
29105
29881
|
*/
|
|
29106
29882
|
getOriginator(poolId) {
|
|
29107
29883
|
return __awaiter(this, void 0, void 0, function* () {
|
|
29108
|
-
var _a;
|
|
29109
29884
|
const candidates = new Set([
|
|
29110
29885
|
poolId.toLowerCase(),
|
|
29111
29886
|
this._metadataIdOf(poolId),
|
|
29112
29887
|
]);
|
|
29113
|
-
const profiles = yield this.
|
|
29114
|
-
|
|
29888
|
+
const profiles = yield this._originatorProfiles();
|
|
29889
|
+
const profile = profiles.find((candidate) => candidate.otherKASUPools.some((pool) => candidates.has(String(pool.id).toLowerCase())));
|
|
29890
|
+
// A copy: the profiles are shared between calls (see
|
|
29891
|
+
// `_memoCmsRead`), so a caller sorting or editing its result must
|
|
29892
|
+
// not change what the next call returns.
|
|
29893
|
+
return profile
|
|
29894
|
+
? Object.assign(Object.assign({}, profile), { otherKASUPools: profile.otherKASUPools.map((pool) => (Object.assign({}, pool))) }) : null;
|
|
29895
|
+
});
|
|
29896
|
+
}
|
|
29897
|
+
/**
|
|
29898
|
+
* The credit originator's ENTIRE lending history for a pool — since when,
|
|
29899
|
+
* total originated, loans outstanding, loss rate — read from the profile
|
|
29900
|
+
* `getOriginator` finds, each figure validated. `null` when no profile
|
|
29901
|
+
* lists the pool, or the profile authors none of the four.
|
|
29902
|
+
*
|
|
29903
|
+
* kasu-ui's Details tab shows these as its Credit Originator strip, and
|
|
29904
|
+
* hides the strip when there is no profile. The money figures are
|
|
29905
|
+
* editorial USD on every deployment: never FX-convert them. An XDC pool
|
|
29906
|
+
* resolves `null` today, as it does in kasu-ui (see `getOriginator`).
|
|
29907
|
+
*
|
|
29908
|
+
* Requires `directusUrl`, like every CMS-only read; rejects when the
|
|
29909
|
+
* read fails. Shares its reads with `getOriginator` for 30 seconds per
|
|
29910
|
+
* instance; a failed read is never reused.
|
|
29911
|
+
*/
|
|
29912
|
+
getOriginatorHistory(poolId) {
|
|
29913
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
29914
|
+
const profile = yield this.getOriginator(poolId);
|
|
29915
|
+
if (!profile)
|
|
29916
|
+
return null;
|
|
29917
|
+
const history = {};
|
|
29918
|
+
const since = readCmsStartDate(profile.delegateLendingHistory, this._now(), { unixSeconds: true });
|
|
29919
|
+
if (since !== undefined)
|
|
29920
|
+
history.since = since;
|
|
29921
|
+
const totalOriginated = finiteOrUndefined(profile.totalLoanFundsOriginated);
|
|
29922
|
+
if (totalOriginated !== undefined) {
|
|
29923
|
+
history.totalOriginated = totalOriginated;
|
|
29924
|
+
}
|
|
29925
|
+
const loansOutstanding = finiteOrUndefined(profile.loansUnderManagement);
|
|
29926
|
+
if (loansOutstanding !== undefined) {
|
|
29927
|
+
history.loansOutstanding = loansOutstanding;
|
|
29928
|
+
}
|
|
29929
|
+
const lossRate = finiteOrUndefined(profile.historicLossRate);
|
|
29930
|
+
if (lossRate !== undefined)
|
|
29931
|
+
history.lossRate = lossRate;
|
|
29932
|
+
return Object.keys(history).length > 0 ? history : null;
|
|
29933
|
+
});
|
|
29934
|
+
}
|
|
29935
|
+
/**
|
|
29936
|
+
* The pool's Risk Management groups, from its CMS risk-management record:
|
|
29937
|
+
* each group with its items (name, the pool's description of it split
|
|
29938
|
+
* into lines, the shared tooltip), the groups in `RISK_GROUP_ORDER`
|
|
29939
|
+
* (Security & Recourse, Structural Controls, Financial Covenants,
|
|
29940
|
+
* Reporting Covenants, Key Eligibility Criteria, then any other in CMS
|
|
29941
|
+
* order). See `groupRiskManagementItems`.
|
|
29942
|
+
*
|
|
29943
|
+
* `null` when the pool has no record, or the record has no usable item —
|
|
29944
|
+
* then kasu-ui falls back to `Strategy.securityStructure`. XDC pools
|
|
29945
|
+
* resolve through the metadata mapping to their Base record, like
|
|
29946
|
+
* `getRiskPerformance`.
|
|
29947
|
+
*
|
|
29948
|
+
* REJECTS when the CMS read fails (an outage, or no `directusUrl` —
|
|
29949
|
+
* `NO_DIRECTUS_URL_MESSAGE`), so a failure stays visible rather than
|
|
29950
|
+
* reading as "no items". For what to SHOW, treat a rejection like `null`:
|
|
29951
|
+
* fall back to `Strategy.securityStructure`, which arrives with the
|
|
29952
|
+
* strategy and needs no second read.
|
|
29953
|
+
*
|
|
29954
|
+
* Shares its CMS read with `getRiskPerformance` for 30 seconds per
|
|
29955
|
+
* instance; a failed read is never reused.
|
|
29956
|
+
*/
|
|
29957
|
+
getRiskManagementGroups(poolId) {
|
|
29958
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
29959
|
+
const metadataId = this._metadataIdOf(poolId);
|
|
29960
|
+
const records = yield this._riskManagementRecords();
|
|
29961
|
+
const record = records.find((r) => String(r.poolIdFK).toLowerCase() === metadataId);
|
|
29962
|
+
if (!record || !Array.isArray(record.items))
|
|
29963
|
+
return null;
|
|
29964
|
+
const groups = groupRiskManagementItems(record.items);
|
|
29965
|
+
return groups.length > 0 ? groups : null;
|
|
29115
29966
|
});
|
|
29116
29967
|
}
|
|
29117
29968
|
_metadataIdOf(poolId) {
|
|
@@ -29190,6 +30041,26 @@ class StrategiesFacade {
|
|
|
29190
30041
|
return yield this._dataService.getPoolTvlHistory(poolId, options);
|
|
29191
30042
|
});
|
|
29192
30043
|
}
|
|
30044
|
+
/**
|
|
30045
|
+
* `getPoolTvlHistory` plus `opening` — the newest clearing before the
|
|
30046
|
+
* window, read at its own block like every other point, or `null` when
|
|
30047
|
+
* the pool has none — from the same read. Pass the result to
|
|
30048
|
+
* `combineTvlHistories` as a leg with its `usdRate`: the opening is what
|
|
30049
|
+
* the leg held before its first point, so a window cut at a different
|
|
30050
|
+
* time on another chain does not count it 0. Same options and errors as
|
|
30051
|
+
* `getPoolTvlHistory`; the extra cost is one clearing row (or, for
|
|
30052
|
+
* `fromEpoch`, one small request) and one block.
|
|
30053
|
+
*
|
|
30054
|
+
* ```ts
|
|
30055
|
+
* const leg = await kasu.strategies.getPoolTvlHistoryWindow(poolId, { count: 52 });
|
|
30056
|
+
* // { entries: PoolTvlHistoryEntry[], opening: PoolTvlHistoryEntry | null }
|
|
30057
|
+
* ```
|
|
30058
|
+
*/
|
|
30059
|
+
getPoolTvlHistoryWindow(poolId_1) {
|
|
30060
|
+
return __awaiter(this, arguments, void 0, function* (poolId, options = {}) {
|
|
30061
|
+
return yield this._dataService.getPoolTvlHistoryWindow(poolId, options);
|
|
30062
|
+
});
|
|
30063
|
+
}
|
|
29193
30064
|
/**
|
|
29194
30065
|
* Fetch a single strategy by pool ID. Returns `null` if not found.
|
|
29195
30066
|
*/
|
|
@@ -29265,10 +30136,39 @@ class StrategiesFacade {
|
|
|
29265
30136
|
apyStructure: pool.poolApyStructure,
|
|
29266
30137
|
thumbnailUrl: pool.thumbnailImageUrl,
|
|
29267
30138
|
bannerUrl: pool.bannerImageUrl,
|
|
30139
|
+
strategyHistory: strategyHistoryOf(pool, this._now()),
|
|
30140
|
+
endBorrowers: endBorrowersOf(pool),
|
|
30141
|
+
securityStructure: securityStructureOf(pool.security),
|
|
29268
30142
|
_raw: pool,
|
|
29269
30143
|
};
|
|
29270
30144
|
}
|
|
29271
30145
|
}
|
|
30146
|
+
/**
|
|
30147
|
+
* How long a CMS-wide read is shared between the pool getters. Long enough
|
|
30148
|
+
* for one screen's worth of calls, short enough that a CMS edit shows on the
|
|
30149
|
+
* next visit; apps cache further on their own terms.
|
|
30150
|
+
*/
|
|
30151
|
+
const CMS_READ_TTL_MS = 30000;
|
|
30152
|
+
/** `Strategy.strategyHistory`: an absent figure is an absent key. */
|
|
30153
|
+
function strategyHistoryOf(pool, now) {
|
|
30154
|
+
const history = {};
|
|
30155
|
+
const since = readCmsStartDate(pool.operatingSince, now);
|
|
30156
|
+
if (since !== undefined)
|
|
30157
|
+
history.since = since;
|
|
30158
|
+
const lenderInterestEarned = finiteOrUndefined(pool.yieldEarned);
|
|
30159
|
+
if (lenderInterestEarned !== undefined) {
|
|
30160
|
+
history.lenderInterestEarned = lenderInterestEarned;
|
|
30161
|
+
}
|
|
30162
|
+
return history;
|
|
30163
|
+
}
|
|
30164
|
+
/** Business Types and Facility Type, or `undefined` when neither is authored. */
|
|
30165
|
+
function endBorrowersOf(pool) {
|
|
30166
|
+
const businessTypes = splitBusinessTypes(pool.industryExposure);
|
|
30167
|
+
const facility = parseFacility(pool.loanStructure);
|
|
30168
|
+
if (!businessTypes && !facility)
|
|
30169
|
+
return undefined;
|
|
30170
|
+
return Object.assign(Object.assign({}, (businessTypes ? { businessTypes } : {})), (facility ? { facility } : {}));
|
|
30171
|
+
}
|
|
29272
30172
|
/**
|
|
29273
30173
|
* A CMS text field as a string, or `undefined` when it is absent, not a
|
|
29274
30174
|
* string, or empty (blank included) — the CMS stores an emptied field as `''`, and an empty
|
|
@@ -30019,6 +30919,102 @@ function normaliseMapping(mapping) {
|
|
|
30019
30919
|
return out;
|
|
30020
30920
|
}
|
|
30021
30921
|
|
|
30922
|
+
/** What `strategyDeployments` throws for an id it cannot place. */
|
|
30923
|
+
class StrategyDeploymentInputError extends Error {
|
|
30924
|
+
constructor(code) {
|
|
30925
|
+
super(`strategyDeployments: input refused (${code})`);
|
|
30926
|
+
this.name = 'StrategyDeploymentInputError';
|
|
30927
|
+
this.code = code;
|
|
30928
|
+
Object.setPrototypeOf(this, StrategyDeploymentInputError.prototype);
|
|
30929
|
+
}
|
|
30930
|
+
}
|
|
30931
|
+
/** The canonical deployment: its pool ids ARE the strategies' ids. */
|
|
30932
|
+
const CANONICAL_CHAIN = 'base';
|
|
30933
|
+
const ADDRESS_RE = /^0x[0-9a-f]{40}$/;
|
|
30934
|
+
/** The deployment's mapping with both sides lowercased; Base never has one. */
|
|
30935
|
+
function mappingOf(configs, chain) {
|
|
30936
|
+
var _a;
|
|
30937
|
+
if (chain === CANONICAL_CHAIN)
|
|
30938
|
+
return [];
|
|
30939
|
+
return Object.entries((_a = configs[chain].poolMetadataMapping) !== null && _a !== void 0 ? _a : {}).map(([local, canonical]) => [local.toLowerCase(), canonical.toLowerCase()]);
|
|
30940
|
+
}
|
|
30941
|
+
/**
|
|
30942
|
+
* `strategyDeployments` over any configs — the export reads `CHAIN_CONFIGS`;
|
|
30943
|
+
* the spec passes its own to cover shapes the built-in mapping lacks.
|
|
30944
|
+
*/
|
|
30945
|
+
function resolveStrategyDeployments(configs, poolId, chain) {
|
|
30946
|
+
const id = typeof poolId === 'string' ? poolId.trim().toLowerCase() : '';
|
|
30947
|
+
if (!ADDRESS_RE.test(id)) {
|
|
30948
|
+
throw new StrategyDeploymentInputError('pool-id');
|
|
30949
|
+
}
|
|
30950
|
+
if (typeof chain !== 'string' ||
|
|
30951
|
+
!Object.prototype.hasOwnProperty.call(configs, chain)) {
|
|
30952
|
+
throw new StrategyDeploymentInputError('chain');
|
|
30953
|
+
}
|
|
30954
|
+
const chains = Object.keys(configs);
|
|
30955
|
+
const mappings = chains.map((c) => [c, mappingOf(configs, c)]);
|
|
30956
|
+
const isLocalElsewhere = mappings.some(([c, mapping]) => c !== chain && mapping.some(([local]) => local === id));
|
|
30957
|
+
let canonical;
|
|
30958
|
+
if (chain === CANONICAL_CHAIN) {
|
|
30959
|
+
// A Base id is its own Base pool — unless the mapping says it is
|
|
30960
|
+
// another deployment's chain-local id, which names no Base pool.
|
|
30961
|
+
if (isLocalElsewhere) {
|
|
30962
|
+
throw new StrategyDeploymentInputError('wrong-chain');
|
|
30963
|
+
}
|
|
30964
|
+
canonical = id;
|
|
30965
|
+
}
|
|
30966
|
+
else {
|
|
30967
|
+
const own = mappingOf(configs, chain).find(([local]) => local === id);
|
|
30968
|
+
if (own) {
|
|
30969
|
+
canonical = own[1];
|
|
30970
|
+
}
|
|
30971
|
+
else {
|
|
30972
|
+
const isCanonical = mappings.some(([, mapping]) => mapping.some(([, target]) => target === id));
|
|
30973
|
+
if (isLocalElsewhere || isCanonical) {
|
|
30974
|
+
throw new StrategyDeploymentInputError('wrong-chain');
|
|
30975
|
+
}
|
|
30976
|
+
// Unmapped off Base: a strategy of one on its own chain (every
|
|
30977
|
+
// XDC USDC pool today). Claiming a Base counterpart would read
|
|
30978
|
+
// Base with an XDC address.
|
|
30979
|
+
return [{ chain, poolId: id }];
|
|
30980
|
+
}
|
|
30981
|
+
}
|
|
30982
|
+
const found = [
|
|
30983
|
+
{ chain: CANONICAL_CHAIN, poolId: canonical },
|
|
30984
|
+
];
|
|
30985
|
+
for (const [c, mapping] of mappings) {
|
|
30986
|
+
for (const [local, target] of mapping) {
|
|
30987
|
+
if (target === canonical)
|
|
30988
|
+
found.push({ chain: c, poolId: local });
|
|
30989
|
+
}
|
|
30990
|
+
}
|
|
30991
|
+
return found;
|
|
30992
|
+
}
|
|
30993
|
+
/**
|
|
30994
|
+
* Every deployment a strategy has a pool on, canonical (Base) first, then the
|
|
30995
|
+
* others in `CHAIN_CONFIGS` order — the legs to fetch and pass to
|
|
30996
|
+
* `combineTvlHistories`. One entry for a strategy that lives on one chain
|
|
30997
|
+
* only (PayFi, on Base).
|
|
30998
|
+
*
|
|
30999
|
+
* `chain` is the deployment `poolId` was read on — required, because an
|
|
31000
|
+
* address alone does not say which chain it belongs to. From either side of
|
|
31001
|
+
* a mapping the answer is the same set: Tax Pay's Base id on `'base'` and
|
|
31002
|
+
* its XDC id on `'xdc'` both return both pools. An id the mapping does not
|
|
31003
|
+
* know is a strategy of one on its own chain. Ids are compared
|
|
31004
|
+
* case-insensitively and returned lowercase.
|
|
31005
|
+
*
|
|
31006
|
+
* Throws `StrategyDeploymentInputError` for a malformed id, an unknown chain,
|
|
31007
|
+
* or an id the mapping places on another deployment (`'wrong-chain'`).
|
|
31008
|
+
*
|
|
31009
|
+
* ```ts
|
|
31010
|
+
* strategyDeployments(strategy.id, 'base');
|
|
31011
|
+
* // [{ chain: 'base', poolId: '0x…' }, { chain: 'xdc', poolId: '0x…' }]
|
|
31012
|
+
* ```
|
|
31013
|
+
*/
|
|
31014
|
+
function strategyDeployments(poolId, chain) {
|
|
31015
|
+
return resolveStrategyDeployments(CHAIN_CONFIGS, poolId, chain);
|
|
31016
|
+
}
|
|
31017
|
+
|
|
30022
31018
|
/** Directus instance holding Kasu's pool editorial content. */
|
|
30023
31019
|
const DEFAULT_DIRECTUS_URL = 'https://kasu-finance.directus.app/';
|
|
30024
31020
|
/**
|
|
@@ -30712,6 +31708,7 @@ exports.PoolTvlHistoryAbortedError = PoolTvlHistoryAbortedError;
|
|
|
30712
31708
|
exports.PoolTvlHistoryTimeoutError = PoolTvlHistoryTimeoutError;
|
|
30713
31709
|
exports.PortfolioFacade = PortfolioFacade;
|
|
30714
31710
|
exports.READ_ONLY_MESSAGE = READ_ONLY_MESSAGE;
|
|
31711
|
+
exports.RISK_GROUP_ORDER = RISK_GROUP_ORDER;
|
|
30715
31712
|
exports.RIXON_CAPITAL = RIXON_CAPITAL;
|
|
30716
31713
|
exports.SECONDS_PER_WEEK = SECONDS_PER_WEEK;
|
|
30717
31714
|
exports.SPONSORED_CALL_KINDS = SPONSORED_CALL_KINDS;
|
|
@@ -30719,6 +31716,9 @@ exports.SPONSORED_CALL_SELECTORS = SPONSORED_CALL_SELECTORS;
|
|
|
30719
31716
|
exports.SdkConfig = SdkConfig;
|
|
30720
31717
|
exports.SponsoredCallInputError = SponsoredCallInputError;
|
|
30721
31718
|
exports.StrategiesFacade = StrategiesFacade;
|
|
31719
|
+
exports.StrategyDeploymentInputError = StrategyDeploymentInputError;
|
|
31720
|
+
exports.StrategyTotalsInputError = StrategyTotalsInputError;
|
|
31721
|
+
exports.TvlHistoryInputError = TvlHistoryInputError;
|
|
30722
31722
|
exports.UPPER_MEZZANINE = UPPER_MEZZANINE;
|
|
30723
31723
|
exports.WithdrawFlow = WithdrawFlow;
|
|
30724
31724
|
exports.accruedInterest = accruedInterest;
|
|
@@ -30738,6 +31738,7 @@ exports.ceilToCents = ceilToCents;
|
|
|
30738
31738
|
exports.checkBatchOutcome = checkBatchOutcome;
|
|
30739
31739
|
exports.checkSponsoredCallOutcome = checkSponsoredCallOutcome;
|
|
30740
31740
|
exports.classifyWalletFailure = classifyWalletFailure;
|
|
31741
|
+
exports.combineTvlHistories = combineTvlHistories;
|
|
30741
31742
|
exports.compareTrancheSeniority = compareTrancheSeniority;
|
|
30742
31743
|
exports.computeSettlementWindow = computeSettlementWindow;
|
|
30743
31744
|
exports.countSubmissions = countSubmissions;
|
|
@@ -30765,6 +31766,7 @@ exports.getCreditOriginator = getCreditOriginator;
|
|
|
30765
31766
|
exports.getInstitutionalLender = getInstitutionalLender;
|
|
30766
31767
|
exports.getStrategyRegion = getStrategyRegion;
|
|
30767
31768
|
exports.getTrancheDisplayName = getTrancheDisplayName;
|
|
31769
|
+
exports.groupRiskManagementItems = groupRiskManagementItems;
|
|
30768
31770
|
exports.isAuMinimumExempt = isAuMinimumExempt;
|
|
30769
31771
|
exports.isAustralianKyc = isAustralianKyc;
|
|
30770
31772
|
exports.isBelowMinimumCapacity = isBelowMinimumCapacity;
|
|
@@ -30780,6 +31782,7 @@ exports.maxNetRateCeiling = maxNetRateCeiling;
|
|
|
30780
31782
|
exports.netEffectiveApy = netEffectiveApy;
|
|
30781
31783
|
exports.netTrancheApyBounds = netTrancheApyBounds;
|
|
30782
31784
|
exports.nextCycleBoundary = nextCycleBoundary;
|
|
31785
|
+
exports.parseFacility = parseFacility;
|
|
30783
31786
|
exports.parseFormattedMessage = parseFormattedMessage;
|
|
30784
31787
|
exports.parseMinorUnits = parseMinorUnits;
|
|
30785
31788
|
exports.parseTrancheBound = parseTrancheBound;
|
|
@@ -30792,9 +31795,15 @@ exports.poolTvl = poolTvl;
|
|
|
30792
31795
|
exports.projectedAnnualEarnings = projectedAnnualEarnings;
|
|
30793
31796
|
exports.proveDepositInReceipt = proveDepositInReceipt;
|
|
30794
31797
|
exports.proveUserOperationInReceipt = proveUserOperationInReceipt;
|
|
31798
|
+
exports.readCmsStartDate = readCmsStartDate;
|
|
30795
31799
|
exports.resolveBoundShortcuts = resolveBoundShortcuts;
|
|
30796
31800
|
exports.resolveDepositBounds = resolveDepositBounds;
|
|
31801
|
+
exports.securityStructureOf = securityStructureOf;
|
|
30797
31802
|
exports.selectVisiblePools = selectVisiblePools;
|
|
31803
|
+
exports.splitBusinessTypes = splitBusinessTypes;
|
|
31804
|
+
exports.splitNumberedClauses = splitNumberedClauses;
|
|
31805
|
+
exports.strategyDeployments = strategyDeployments;
|
|
31806
|
+
exports.strategyTotalsUsd = strategyTotalsUsd;
|
|
30798
31807
|
exports.submissionEvents = submissionEvents;
|
|
30799
31808
|
exports.submitBatch = submitBatch;
|
|
30800
31809
|
exports.submitSponsoredCall = submitSponsoredCall;
|