@kasufinance/kasu-sdk 2.4.2 → 2.6.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/LICENSE +21 -0
- package/README.md +201 -253
- package/dist/bundle.cjs.js +5472 -1710
- package/dist/bundle.esm.js +5415 -1713
- package/dist/domain/au-minimum.d.ts +135 -0
- package/dist/domain/au-minimum.js +154 -0
- package/dist/domain/au-minimum.js.map +1 -0
- package/dist/domain/au-minimum.test.js +202 -0
- package/dist/domain/au-minimum.test.js.map +1 -0
- package/dist/domain/deposit-bounds.d.ts +62 -0
- package/dist/domain/deposit-bounds.js +84 -0
- package/dist/domain/deposit-bounds.js.map +1 -0
- package/dist/domain/deposit-bounds.test.d.ts +1 -0
- package/dist/domain/deposit-bounds.test.js +135 -0
- package/dist/domain/deposit-bounds.test.js.map +1 -0
- package/dist/domain/index.d.ts +40 -0
- package/dist/domain/index.js +33 -0
- package/dist/domain/index.js.map +1 -0
- package/dist/domain/loan-contract.d.ts +174 -0
- package/dist/domain/loan-contract.js +160 -0
- package/dist/domain/loan-contract.js.map +1 -0
- package/dist/domain/loan-contract.test.d.ts +1 -0
- package/dist/domain/loan-contract.test.js +255 -0
- package/dist/domain/loan-contract.test.js.map +1 -0
- package/dist/domain/partners.d.ts +34 -0
- package/dist/domain/partners.js +43 -0
- package/dist/domain/partners.js.map +1 -0
- package/dist/domain/partners.test.d.ts +1 -0
- package/dist/domain/partners.test.js +53 -0
- package/dist/domain/partners.test.js.map +1 -0
- package/dist/domain/pools.d.ts +70 -0
- package/dist/domain/pools.js +134 -0
- package/dist/domain/pools.js.map +1 -0
- package/dist/domain/pools.test.d.ts +1 -0
- package/dist/domain/pools.test.js +184 -0
- package/dist/domain/pools.test.js.map +1 -0
- package/dist/domain/rates.d.ts +83 -0
- package/dist/domain/rates.js +120 -0
- package/dist/domain/rates.js.map +1 -0
- package/dist/domain/rates.test.d.ts +1 -0
- package/dist/domain/rates.test.js +181 -0
- package/dist/domain/rates.test.js.map +1 -0
- package/dist/domain/requests.d.ts +181 -0
- package/dist/domain/requests.js +202 -0
- package/dist/domain/requests.js.map +1 -0
- package/dist/domain/requests.test.d.ts +1 -0
- package/dist/domain/requests.test.js +470 -0
- package/dist/domain/requests.test.js.map +1 -0
- package/dist/domain/settlement.d.ts +97 -0
- package/dist/domain/settlement.js +117 -0
- package/dist/domain/settlement.js.map +1 -0
- package/dist/domain/settlement.test.d.ts +1 -0
- package/dist/domain/settlement.test.js +152 -0
- package/dist/domain/settlement.test.js.map +1 -0
- package/dist/domain/tranche-display-name.d.ts +17 -0
- package/dist/domain/tranche-display-name.js +30 -0
- package/dist/domain/tranche-display-name.js.map +1 -0
- package/dist/domain/tranche-display-name.test.d.ts +1 -0
- package/dist/domain/tranche-display-name.test.js +58 -0
- package/dist/domain/tranche-display-name.test.js.map +1 -0
- package/dist/domain/tranches.d.ts +89 -0
- package/dist/domain/tranches.js +150 -0
- package/dist/domain/tranches.js.map +1 -0
- package/dist/domain/tranches.test.d.ts +1 -0
- package/dist/domain/tranches.test.js +206 -0
- package/dist/domain/tranches.test.js.map +1 -0
- package/dist/domain/wallet-errors.d.ts +37 -0
- package/dist/domain/wallet-errors.js +56 -0
- package/dist/domain/wallet-errors.js.map +1 -0
- package/dist/domain/wallet-errors.test.d.ts +1 -0
- package/dist/domain/wallet-errors.test.js +71 -0
- package/dist/domain/wallet-errors.test.js.map +1 -0
- package/dist/facade/chain-configs.js +61 -1
- package/dist/facade/chain-configs.js.map +1 -1
- package/dist/facade/config.test.d.ts +1 -0
- package/dist/facade/config.test.js +216 -0
- package/dist/facade/config.test.js.map +1 -0
- package/dist/facade/deposits.d.ts +22 -1
- package/dist/facade/deposits.js +24 -1
- package/dist/facade/deposits.js.map +1 -1
- package/dist/facade/facade.test.d.ts +1 -0
- package/dist/{tests → facade}/facade.test.js +87 -5
- package/dist/facade/facade.test.js.map +1 -0
- package/dist/facade/index.d.ts +2 -1
- package/dist/facade/index.js +2 -0
- package/dist/facade/index.js.map +1 -1
- package/dist/facade/kasu.d.ts +49 -8
- package/dist/facade/kasu.js +117 -13
- package/dist/facade/kasu.js.map +1 -1
- package/dist/facade/strategies.d.ts +25 -0
- package/dist/facade/strategies.js +36 -0
- package/dist/facade/strategies.js.map +1 -1
- package/dist/facade/types.d.ts +57 -2
- package/dist/facade/unused-pool-ids.d.ts +31 -0
- package/dist/facade/unused-pool-ids.js +57 -0
- package/dist/facade/unused-pool-ids.js.map +1 -0
- package/dist/facade/user-portfolio.d.ts +18 -0
- package/dist/facade/user-portfolio.js +23 -0
- package/dist/facade/user-portfolio.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/sdk-config.d.ts +11 -0
- package/dist/sdk-config.js +8 -1
- package/dist/sdk-config.js.map +1 -1
- package/dist/services/DataService/data-service.d.ts +5 -0
- package/dist/services/DataService/data-service.js +10 -12
- package/dist/services/DataService/data-service.js.map +1 -1
- package/dist/services/DataService/directus-client.d.ts +26 -0
- package/dist/services/DataService/directus-client.js +38 -0
- package/dist/services/DataService/directus-client.js.map +1 -0
- package/dist/services/Locking/calculate-apy.test.d.ts +1 -0
- package/dist/services/Locking/calculate-apy.test.js +41 -0
- package/dist/services/Locking/calculate-apy.test.js.map +1 -0
- package/dist/services/Locking/locking.d.ts +14 -0
- package/dist/services/Locking/locking.js +16 -2
- package/dist/services/Locking/locking.js.map +1 -1
- package/dist/services/UserLending/user-lending.js +11 -7
- package/dist/services/UserLending/user-lending.js.map +1 -1
- package/dist/tests/sample.test.js +46 -25
- package/dist/tests/sample.test.js.map +1 -1
- package/package.json +43 -12
- package/src/domain/au-minimum.test.ts +371 -0
- package/src/domain/au-minimum.ts +192 -0
- package/src/domain/deposit-bounds.test.ts +200 -0
- package/src/domain/deposit-bounds.ts +118 -0
- package/src/domain/index.ts +134 -0
- package/src/domain/loan-contract.test.ts +343 -0
- package/src/domain/loan-contract.ts +275 -0
- package/src/domain/partners.test.ts +83 -0
- package/src/domain/partners.ts +66 -0
- package/src/domain/pools.test.ts +260 -0
- package/src/domain/pools.ts +150 -0
- package/src/domain/rates.test.ts +254 -0
- package/src/domain/rates.ts +120 -0
- package/src/domain/requests.test.ts +653 -0
- package/src/domain/requests.ts +414 -0
- package/src/domain/settlement.test.ts +198 -0
- package/src/domain/settlement.ts +161 -0
- package/src/domain/tranche-display-name.test.ts +96 -0
- package/src/domain/tranche-display-name.ts +34 -0
- package/src/domain/tranches.test.ts +300 -0
- package/src/domain/tranches.ts +177 -0
- package/src/domain/wallet-errors.test.ts +100 -0
- package/src/domain/wallet-errors.ts +56 -0
- package/src/facade/chain-configs.ts +62 -1
- package/src/facade/config.test.ts +265 -0
- package/src/facade/deposits.ts +25 -0
- package/src/{tests → facade}/facade.test.ts +134 -5
- package/src/facade/index.ts +4 -0
- package/src/facade/kasu.ts +155 -12
- package/src/facade/strategies.ts +36 -0
- package/src/facade/types.ts +60 -2
- package/src/facade/unused-pool-ids.ts +59 -0
- package/src/facade/user-portfolio.ts +24 -0
- package/src/index.ts +8 -0
- package/src/sdk-config.ts +19 -1
- package/src/services/DataService/data-service.ts +14 -26
- package/src/services/DataService/directus-client.ts +54 -0
- package/src/services/Locking/calculate-apy.test.ts +52 -0
- package/src/services/Locking/locking.ts +16 -2
- package/src/services/UserLending/user-lending.ts +17 -21
- package/src/tests/sample.test.ts +54 -33
- package/.eslintignore +0 -3
- package/.eslintrc.json +0 -90
- package/.github/workflows/ci.yml +0 -39
- package/.github/workflows/gitleaks.yml +0 -22
- package/.github/workflows/main.yml +0 -41
- package/.gitleaksignore +0 -19
- package/.prettierignore +0 -3
- package/.prettierrc +0 -28
- package/CLAUDE.md +0 -410
- package/copy-abi.ps1 +0 -15
- package/dist/tests/facade.test.js.map +0 -1
- package/index.ts +0 -2
- package/jest-config.ts +0 -9
- package/jest.config.js +0 -8
- package/rollup.config.js +0 -45
- package/tsconfig.json +0 -27
- /package/dist/{tests/facade.test.d.ts → domain/au-minimum.test.d.ts} +0 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cycles and the clearing window — the protocol's weekly clock, as numbers.
|
|
3
|
+
*
|
|
4
|
+
* Lifted verbatim from kasu-ui's `features/portfolio/lib/settlement-window.ts`
|
|
5
|
+
* and `features/lending/lib/cycle-dates.ts`. The two formatters that live
|
|
6
|
+
* beside `deriveCycleDates` there (`formatCycleDate`, `formatCycleCloseUtc`)
|
|
7
|
+
* print words and stay in the applications; everything here is unix seconds in
|
|
8
|
+
* and unix seconds out.
|
|
9
|
+
*
|
|
10
|
+
* The clearing window is the fixed 48 hours immediately preceding an epoch
|
|
11
|
+
* end. Inside it, pending requests are being processed and cannot be modified,
|
|
12
|
+
* and the countdown runs to the epoch end. Outside it, the countdown runs to
|
|
13
|
+
* the next clearing-window start.
|
|
14
|
+
*
|
|
15
|
+
* The epoch end comes straight from the chain (`nextEpochStartTimestamp`, i.e.
|
|
16
|
+
* the SDK's `getNextEpochDate`) — the same value the protocol's own
|
|
17
|
+
* `getNextClearingPeriodDate` derives from — so the window always lines up
|
|
18
|
+
* with the real weekly schedule (Tue 06:00 → Thu 06:00 UTC on Base) instead of
|
|
19
|
+
* a projected subgraph timestamp that can drift off the grid.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Clearing-window length — a fixed 48h protocol constant. Exported so every
|
|
23
|
+
* consumer derives the window from the same number this module runs on.
|
|
24
|
+
*/
|
|
25
|
+
export const CLEARING_WINDOW_SECONDS = 48 * 60 * 60;
|
|
26
|
+
/**
|
|
27
|
+
* Weekly cadence — the epoch schedule is fixed weekly (Tue → Thu UTC on Base).
|
|
28
|
+
* Used only to roll a cycle forward when a request lands inside a window that
|
|
29
|
+
* has already closed.
|
|
30
|
+
*/
|
|
31
|
+
const WEEK_SECONDS = 7 * 24 * 60 * 60;
|
|
32
|
+
/**
|
|
33
|
+
* Which phase of the weekly cycle `nowSeconds` falls in, and how long is left
|
|
34
|
+
* of it.
|
|
35
|
+
*
|
|
36
|
+
* `'unknown'` when no epoch boundary has been loaded yet, or when the one on
|
|
37
|
+
* hand is stale (it has already elapsed — the on-chain value refetches to the
|
|
38
|
+
* next boundary shortly after rollover). A caller must render its "no cycle
|
|
39
|
+
* loaded" state there, never a zeroed countdown.
|
|
40
|
+
*/
|
|
41
|
+
export function computeSettlementWindow({ nowSeconds, nextEpochStart, clearingWindowSeconds = CLEARING_WINDOW_SECONDS, }) {
|
|
42
|
+
// No epoch boundary loaded yet, or a stale one that already elapsed.
|
|
43
|
+
if (nextEpochStart <= 0 || nextEpochStart <= nowSeconds) {
|
|
44
|
+
return { phase: 'unknown' };
|
|
45
|
+
}
|
|
46
|
+
const clearingStart = nextEpochStart - clearingWindowSeconds;
|
|
47
|
+
if (nowSeconds < clearingStart) {
|
|
48
|
+
return {
|
|
49
|
+
phase: 'awaiting',
|
|
50
|
+
secondsUntilClearing: clearingStart - nowSeconds,
|
|
51
|
+
nextClearingStart: clearingStart,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
// nowSeconds is in [clearingStart, nextEpochStart) — inside the window.
|
|
55
|
+
return {
|
|
56
|
+
phase: 'clearing',
|
|
57
|
+
secondsUntilEpochEnd: nextEpochStart - nowSeconds,
|
|
58
|
+
epochEnd: nextEpochStart,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The next cycle boundary strictly after `nowSeconds`, in unix seconds — the
|
|
63
|
+
* cycle close (`nextEpochStart − 48h`) while the window is still open, the
|
|
64
|
+
* epoch end once we are inside it.
|
|
65
|
+
*
|
|
66
|
+
* `undefined` when there is no boundary left to wait for: no epoch boundary
|
|
67
|
+
* loaded, or a cached one that has already elapsed — the same staleness rule
|
|
68
|
+
* `computeSettlementWindow` applies before it reports `'unknown'`.
|
|
69
|
+
*
|
|
70
|
+
* Split out of the state machine because some consumers need the INSTANT
|
|
71
|
+
* rather than the phase: one to flush the cycle-dependent caches when the
|
|
72
|
+
* clock crosses it, one to move a pre-commit screen's snapshot clock at the
|
|
73
|
+
* same moment.
|
|
74
|
+
*/
|
|
75
|
+
export function nextCycleBoundary(nextEpochStart, nowSeconds, clearingWindowSeconds = CLEARING_WINDOW_SECONDS) {
|
|
76
|
+
if (!nextEpochStart || nextEpochStart <= 0)
|
|
77
|
+
return undefined;
|
|
78
|
+
const clearingStart = nextEpochStart - clearingWindowSeconds;
|
|
79
|
+
if (nowSeconds < clearingStart)
|
|
80
|
+
return clearingStart;
|
|
81
|
+
if (nowSeconds < nextEpochStart)
|
|
82
|
+
return nextEpochStart;
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The cycle-close and outcome dates for a request submitted `now`.
|
|
87
|
+
*
|
|
88
|
+
* - The cycle "closes" (stops accepting requests, starts processing) at the
|
|
89
|
+
* start of the 48h clearing window, i.e. 48h before the epoch end.
|
|
90
|
+
* - Processing takes up to 48h, so the outcome is confirmed by the epoch end
|
|
91
|
+
* (close + 48h).
|
|
92
|
+
*
|
|
93
|
+
* Returns `null` when the epoch boundary is not available or is stale — a
|
|
94
|
+
* caller then omits the dates entirely (omit, don't stub).
|
|
95
|
+
*
|
|
96
|
+
* The common case is a request submitted OUTSIDE the clearing window: the
|
|
97
|
+
* close is `nextEpochStart − 48h` and the outcome is `nextEpochStart`. When
|
|
98
|
+
* the request lands INSIDE the current clearing window (that close is already
|
|
99
|
+
* in the past), it queues for the NEXT weekly cycle, so the close is advanced
|
|
100
|
+
* by whole weeks until it is in the future.
|
|
101
|
+
*/
|
|
102
|
+
export function deriveCycleDates(nextEpochStart, nowSeconds) {
|
|
103
|
+
// No boundary loaded, or a stale one that already elapsed — the same
|
|
104
|
+
// staleness rule as `computeSettlementWindow`.
|
|
105
|
+
if (!nextEpochStart || nextEpochStart <= nowSeconds)
|
|
106
|
+
return null;
|
|
107
|
+
let close = nextEpochStart - CLEARING_WINDOW_SECONDS;
|
|
108
|
+
let outcome = nextEpochStart;
|
|
109
|
+
// Inside the current clearing window the close already passed; a request
|
|
110
|
+
// now is queued for the next weekly cycle.
|
|
111
|
+
while (close <= nowSeconds) {
|
|
112
|
+
close += WEEK_SECONDS;
|
|
113
|
+
outcome += WEEK_SECONDS;
|
|
114
|
+
}
|
|
115
|
+
return { close, outcome };
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=settlement.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settlement.js","sourceRoot":"","sources":["../../src/domain/settlement.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH;;;GAGG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC;AAEpD;;;;GAIG;AACH,MAAM,YAAY,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC;AAwBtC;;;;;;;;GAQG;AACH,MAAM,UAAU,uBAAuB,CAAC,EACpC,UAAU,EACV,cAAc,EACd,qBAAqB,GAAG,uBAAuB,GAC3B;IACpB,qEAAqE;IACrE,IAAI,cAAc,IAAI,CAAC,IAAI,cAAc,IAAI,UAAU,EAAE,CAAC;QACtD,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC;IAChC,CAAC;IAED,MAAM,aAAa,GAAG,cAAc,GAAG,qBAAqB,CAAC;IAE7D,IAAI,UAAU,GAAG,aAAa,EAAE,CAAC;QAC7B,OAAO;YACH,KAAK,EAAE,UAAU;YACjB,oBAAoB,EAAE,aAAa,GAAG,UAAU;YAChD,iBAAiB,EAAE,aAAa;SACnC,CAAC;IACN,CAAC;IAED,wEAAwE;IACxE,OAAO;QACH,KAAK,EAAE,UAAU;QACjB,oBAAoB,EAAE,cAAc,GAAG,UAAU;QACjD,QAAQ,EAAE,cAAc;KAC3B,CAAC;AACN,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,iBAAiB,CAC7B,cAAkC,EAClC,UAAkB,EAClB,wBAAgC,uBAAuB;IAEvD,IAAI,CAAC,cAAc,IAAI,cAAc,IAAI,CAAC;QAAE,OAAO,SAAS,CAAC;IAC7D,MAAM,aAAa,GAAG,cAAc,GAAG,qBAAqB,CAAC;IAC7D,IAAI,UAAU,GAAG,aAAa;QAAE,OAAO,aAAa,CAAC;IACrD,IAAI,UAAU,GAAG,cAAc;QAAE,OAAO,cAAc,CAAC;IACvD,OAAO,SAAS,CAAC;AACrB,CAAC;AASD;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,gBAAgB,CAC5B,cAAkC,EAClC,UAAkB;IAElB,qEAAqE;IACrE,+CAA+C;IAC/C,IAAI,CAAC,cAAc,IAAI,cAAc,IAAI,UAAU;QAAE,OAAO,IAAI,CAAC;IAEjE,IAAI,KAAK,GAAG,cAAc,GAAG,uBAAuB,CAAC;IACrD,IAAI,OAAO,GAAG,cAAc,CAAC;IAC7B,yEAAyE;IACzE,2CAA2C;IAC3C,OAAO,KAAK,IAAI,UAAU,EAAE,CAAC;QACzB,KAAK,IAAI,YAAY,CAAC;QACtB,OAAO,IAAI,YAAY,CAAC;IAC5B,CAAC;IACD,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC;AAC9B,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { CLEARING_WINDOW_SECONDS, computeSettlementWindow, deriveCycleDates, nextCycleBoundary, } from './settlement';
|
|
2
|
+
/**
|
|
3
|
+
* Ported from kasu-ui `src/features/portfolio/lib/settlement-window.test.ts`
|
|
4
|
+
* and the non-format half of
|
|
5
|
+
* `src/features/lending/lib/cycle-dates.test.ts`. The `formatCycleDate` /
|
|
6
|
+
* `formatCycleCloseUtc` cases stay there — they assert printed words.
|
|
7
|
+
*/
|
|
8
|
+
const HOUR = 60 * 60;
|
|
9
|
+
const DAY = 24 * HOUR;
|
|
10
|
+
const CLEARING = 48 * HOUR;
|
|
11
|
+
const WEEK = 7 * DAY;
|
|
12
|
+
describe('computeSettlementWindow', () => {
|
|
13
|
+
it('returns "unknown" when the epoch boundary is missing', () => {
|
|
14
|
+
expect(computeSettlementWindow({
|
|
15
|
+
nowSeconds: 1000000,
|
|
16
|
+
nextEpochStart: 0,
|
|
17
|
+
})).toEqual({ phase: 'unknown' });
|
|
18
|
+
});
|
|
19
|
+
it('returns "unknown" when the epoch boundary is stale (already elapsed)', () => {
|
|
20
|
+
expect(computeSettlementWindow({
|
|
21
|
+
nowSeconds: 1000000,
|
|
22
|
+
nextEpochStart: 999000,
|
|
23
|
+
})).toEqual({ phase: 'unknown' });
|
|
24
|
+
});
|
|
25
|
+
it('counts down to the clearing window when more than 48h from epoch end', () => {
|
|
26
|
+
const epochEnd = 1700000000; // Thu 06:00 UTC
|
|
27
|
+
const now = epochEnd - 3 * DAY; // 24h before the window opens
|
|
28
|
+
const result = computeSettlementWindow({
|
|
29
|
+
nowSeconds: now,
|
|
30
|
+
nextEpochStart: epochEnd,
|
|
31
|
+
});
|
|
32
|
+
expect(result.phase).toBe('awaiting');
|
|
33
|
+
if (result.phase !== 'awaiting')
|
|
34
|
+
return;
|
|
35
|
+
expect(result.nextClearingStart).toBe(epochEnd - CLEARING); // Tue 06:00
|
|
36
|
+
expect(result.secondsUntilClearing).toBe(DAY);
|
|
37
|
+
});
|
|
38
|
+
it('reports "clearing" only inside the 48h window before epoch end', () => {
|
|
39
|
+
const epochEnd = 1700000000;
|
|
40
|
+
const now = epochEnd - 24 * HOUR; // 24h into the window
|
|
41
|
+
const result = computeSettlementWindow({
|
|
42
|
+
nowSeconds: now,
|
|
43
|
+
nextEpochStart: epochEnd,
|
|
44
|
+
});
|
|
45
|
+
expect(result.phase).toBe('clearing');
|
|
46
|
+
if (result.phase !== 'clearing')
|
|
47
|
+
return;
|
|
48
|
+
expect(result.epochEnd).toBe(epochEnd);
|
|
49
|
+
expect(result.secondsUntilEpochEnd).toBe(24 * HOUR);
|
|
50
|
+
});
|
|
51
|
+
it('flips awaiting → clearing exactly at the window boundary (T-48h)', () => {
|
|
52
|
+
const epochEnd = 1700000000;
|
|
53
|
+
expect(computeSettlementWindow({
|
|
54
|
+
nowSeconds: epochEnd - CLEARING - 1,
|
|
55
|
+
nextEpochStart: epochEnd,
|
|
56
|
+
}).phase).toBe('awaiting');
|
|
57
|
+
expect(computeSettlementWindow({
|
|
58
|
+
nowSeconds: epochEnd - CLEARING,
|
|
59
|
+
nextEpochStart: epochEnd,
|
|
60
|
+
}).phase).toBe('clearing');
|
|
61
|
+
});
|
|
62
|
+
it('respects a non-default clearing window length', () => {
|
|
63
|
+
const epochEnd = 1700000000;
|
|
64
|
+
const now = epochEnd - 36 * HOUR;
|
|
65
|
+
// 36h out is "clearing" under the default 48h window but "awaiting"
|
|
66
|
+
// under a 24h one.
|
|
67
|
+
expect(computeSettlementWindow({
|
|
68
|
+
nowSeconds: now,
|
|
69
|
+
nextEpochStart: epochEnd,
|
|
70
|
+
}).phase).toBe('clearing');
|
|
71
|
+
expect(computeSettlementWindow({
|
|
72
|
+
nowSeconds: now,
|
|
73
|
+
nextEpochStart: epochEnd,
|
|
74
|
+
clearingWindowSeconds: 24 * HOUR,
|
|
75
|
+
}).phase).toBe('awaiting');
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
describe('nextCycleBoundary', () => {
|
|
79
|
+
const epochEnd = 1700000000;
|
|
80
|
+
const clearingStart = epochEnd - CLEARING;
|
|
81
|
+
it('returns the cycle close while the window is still ahead', () => {
|
|
82
|
+
expect(nextCycleBoundary(epochEnd, epochEnd - 3 * DAY)).toBe(clearingStart);
|
|
83
|
+
});
|
|
84
|
+
it('returns the epoch end once inside the clearing window', () => {
|
|
85
|
+
expect(nextCycleBoundary(epochEnd, clearingStart + 1)).toBe(epochEnd);
|
|
86
|
+
});
|
|
87
|
+
it('hands over from close to epoch end exactly at the boundary instant', () => {
|
|
88
|
+
// At T-48h the close has just been reached, so the next thing to wait
|
|
89
|
+
// for is the epoch end — not the instant we are standing on.
|
|
90
|
+
expect(nextCycleBoundary(epochEnd, clearingStart - 1)).toBe(clearingStart);
|
|
91
|
+
expect(nextCycleBoundary(epochEnd, clearingStart)).toBe(epochEnd);
|
|
92
|
+
});
|
|
93
|
+
it('returns undefined when there is nothing left to wait for', () => {
|
|
94
|
+
expect(nextCycleBoundary(epochEnd, epochEnd)).toBeUndefined(); // elapsed
|
|
95
|
+
expect(nextCycleBoundary(epochEnd, epochEnd + DAY)).toBeUndefined(); // stale
|
|
96
|
+
expect(nextCycleBoundary(undefined, epochEnd - DAY)).toBeUndefined(); // not loaded
|
|
97
|
+
expect(nextCycleBoundary(0, epochEnd - DAY)).toBeUndefined(); // sentinel zero
|
|
98
|
+
});
|
|
99
|
+
it('agrees with computeSettlementWindow about where the window opens', () => {
|
|
100
|
+
// One number, two consumers: the phase machine and the timer helper
|
|
101
|
+
// must never disagree about when the cycle turns.
|
|
102
|
+
expect(CLEARING_WINDOW_SECONDS).toBe(CLEARING);
|
|
103
|
+
const justInside = nextCycleBoundary(epochEnd, clearingStart);
|
|
104
|
+
expect(computeSettlementWindow({
|
|
105
|
+
nowSeconds: clearingStart,
|
|
106
|
+
nextEpochStart: epochEnd,
|
|
107
|
+
}).phase).toBe('clearing');
|
|
108
|
+
expect(justInside).toBe(epochEnd);
|
|
109
|
+
});
|
|
110
|
+
it('respects a non-default clearing window length', () => {
|
|
111
|
+
const now = epochEnd - 36 * HOUR;
|
|
112
|
+
// 36h out: inside a 48h window (next stop is the epoch end), still
|
|
113
|
+
// ahead of a 24h one (next stop is that window opening).
|
|
114
|
+
expect(nextCycleBoundary(epochEnd, now)).toBe(epochEnd);
|
|
115
|
+
expect(nextCycleBoundary(epochEnd, now, 24 * HOUR)).toBe(epochEnd - 24 * HOUR);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
describe('deriveCycleDates', () => {
|
|
119
|
+
// Fixture: epoch end = Thu 6 Aug 2026, 06:00 UTC.
|
|
120
|
+
const EPOCH_END = Date.UTC(2026, 7, 6, 6, 0, 0) / 1000; // 1785996000
|
|
121
|
+
const CLOSE = EPOCH_END - CLEARING; // Tue 4 Aug 06:00 UTC
|
|
122
|
+
it('returns close = epochEnd − 48h and outcome = epochEnd before the window opens', () => {
|
|
123
|
+
const now = Date.UTC(2026, 7, 1, 12, 0, 0) / 1000; // Sat 1 Aug
|
|
124
|
+
expect(deriveCycleDates(EPOCH_END, now)).toEqual({
|
|
125
|
+
close: CLOSE,
|
|
126
|
+
outcome: EPOCH_END,
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
it('advances to the next weekly cycle when the request lands inside the clearing window', () => {
|
|
130
|
+
// now is Wed 5 Aug 00:00 — inside [close, epochEnd): this cycle has
|
|
131
|
+
// already closed, so the request queues for next week.
|
|
132
|
+
const now = Date.UTC(2026, 7, 5, 0, 0, 0) / 1000;
|
|
133
|
+
expect(deriveCycleDates(EPOCH_END, now)).toEqual({
|
|
134
|
+
close: CLOSE + WEEK,
|
|
135
|
+
outcome: EPOCH_END + WEEK,
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
it('returns null when the epoch boundary is missing', () => {
|
|
139
|
+
expect(deriveCycleDates(undefined, EPOCH_END - WEEK)).toBeNull();
|
|
140
|
+
expect(deriveCycleDates(0, EPOCH_END - WEEK)).toBeNull();
|
|
141
|
+
});
|
|
142
|
+
it('returns null when the epoch boundary is stale (already elapsed)', () => {
|
|
143
|
+
expect(deriveCycleDates(EPOCH_END, EPOCH_END + 60)).toBeNull();
|
|
144
|
+
});
|
|
145
|
+
it('runs on the same window constant as the phase machine', () => {
|
|
146
|
+
var _a;
|
|
147
|
+
const now = Date.UTC(2026, 7, 1, 12, 0, 0) / 1000;
|
|
148
|
+
const dates = deriveCycleDates(EPOCH_END, now);
|
|
149
|
+
expect(dates === null || dates === void 0 ? void 0 : dates.outcome).toBe(((_a = dates === null || dates === void 0 ? void 0 : dates.close) !== null && _a !== void 0 ? _a : 0) + CLEARING_WINDOW_SECONDS);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
//# sourceMappingURL=settlement.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settlement.test.js","sourceRoot":"","sources":["../../src/domain/settlement.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { PoolNameSignal } from './partners';
|
|
2
|
+
export declare const UPPER_MEZZANINE = "Upper Mezzanine";
|
|
3
|
+
/**
|
|
4
|
+
* User-facing display name for a tranche. Maps `Senior` → `Upper Mezzanine`
|
|
5
|
+
* on Apxium pools (identified by pool name, see `partners.ts`); every other
|
|
6
|
+
* tranche/pool combination passes through unchanged.
|
|
7
|
+
*
|
|
8
|
+
* Call this at the view-model boundary (options, cards, transaction views),
|
|
9
|
+
* NEVER in matching/sorting logic — `compareTrancheSeniority` and
|
|
10
|
+
* `pickDefaultTrancheId` rank by the raw subgraph name and must keep doing so.
|
|
11
|
+
* This is the one function in `domain/` that returns a user-visible string,
|
|
12
|
+
* and it is here rather than in each frontend so all three cannot drift.
|
|
13
|
+
*
|
|
14
|
+
* `pool` accepts the raw `PoolOverview` (`poolName`), the facade `Strategy`
|
|
15
|
+
* (`name`), or the name itself.
|
|
16
|
+
*/
|
|
17
|
+
export declare function getTrancheDisplayName(trancheName: string, pool: PoolNameSignal): string;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { APXIUM, getCreditOriginator } from './partners';
|
|
2
|
+
// Business rename (2026-07): Apxium strategies market their top retail tranche
|
|
3
|
+
// as "Upper Mezzanine" — the true senior position is held by the institutional
|
|
4
|
+
// lender (Rixon Capital), so retail lenders are never actually senior in the
|
|
5
|
+
// waterfall. Display-only: subgraph/SDK tranche names, ids and any
|
|
6
|
+
// seniority-ranking logic keep the on-chain "Senior" name.
|
|
7
|
+
export const UPPER_MEZZANINE = 'Upper Mezzanine';
|
|
8
|
+
const SENIOR = 'senior';
|
|
9
|
+
/**
|
|
10
|
+
* User-facing display name for a tranche. Maps `Senior` → `Upper Mezzanine`
|
|
11
|
+
* on Apxium pools (identified by pool name, see `partners.ts`); every other
|
|
12
|
+
* tranche/pool combination passes through unchanged.
|
|
13
|
+
*
|
|
14
|
+
* Call this at the view-model boundary (options, cards, transaction views),
|
|
15
|
+
* NEVER in matching/sorting logic — `compareTrancheSeniority` and
|
|
16
|
+
* `pickDefaultTrancheId` rank by the raw subgraph name and must keep doing so.
|
|
17
|
+
* This is the one function in `domain/` that returns a user-visible string,
|
|
18
|
+
* and it is here rather than in each frontend so all three cannot drift.
|
|
19
|
+
*
|
|
20
|
+
* `pool` accepts the raw `PoolOverview` (`poolName`), the facade `Strategy`
|
|
21
|
+
* (`name`), or the name itself.
|
|
22
|
+
*/
|
|
23
|
+
export function getTrancheDisplayName(trancheName, pool) {
|
|
24
|
+
if (trancheName.trim().toLowerCase() !== SENIOR)
|
|
25
|
+
return trancheName;
|
|
26
|
+
return getCreditOriginator(pool).name === APXIUM.name
|
|
27
|
+
? UPPER_MEZZANINE
|
|
28
|
+
: trancheName;
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=tranche-display-name.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tranche-display-name.js","sourceRoot":"","sources":["../../src/domain/tranche-display-name.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,mBAAmB,EAAkB,MAAM,YAAY,CAAC;AAEzE,+EAA+E;AAC/E,+EAA+E;AAC/E,6EAA6E;AAC7E,mEAAmE;AACnE,2DAA2D;AAC3D,MAAM,CAAC,MAAM,eAAe,GAAG,iBAAiB,CAAC;AAEjD,MAAM,MAAM,GAAG,QAAQ,CAAC;AAExB;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,qBAAqB,CACjC,WAAmB,EACnB,IAAoB;IAEpB,IAAI,WAAW,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,KAAK,MAAM;QAAE,OAAO,WAAW,CAAC;IACpE,OAAO,mBAAmB,CAAC,IAAI,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC,IAAI;QACjD,CAAC,CAAC,eAAe;QACjB,CAAC,CAAC,WAAW,CAAC;AACtB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { getTrancheDisplayName, UPPER_MEZZANINE } from './tranche-display-name';
|
|
2
|
+
// Ported from kasu-ui `src/features/lending/lib/tranche-display-name.test.ts`.
|
|
3
|
+
describe('getTrancheDisplayName', () => {
|
|
4
|
+
it('renames Senior to Upper Mezzanine on Apxium pools', () => {
|
|
5
|
+
expect(getTrancheDisplayName('Senior', {
|
|
6
|
+
poolName: 'Taxation Funding (Tax Pay)',
|
|
7
|
+
})).toBe(UPPER_MEZZANINE);
|
|
8
|
+
expect(getTrancheDisplayName('Senior', {
|
|
9
|
+
poolName: 'Professional Fee Funding',
|
|
10
|
+
})).toBe(UPPER_MEZZANINE);
|
|
11
|
+
expect(getTrancheDisplayName('Senior', { poolName: 'Whole Ledger Funding' })).toBe(UPPER_MEZZANINE);
|
|
12
|
+
});
|
|
13
|
+
it('keeps Senior on non-Apxium (InvoiceMate) pools', () => {
|
|
14
|
+
expect(getTrancheDisplayName('Senior', {
|
|
15
|
+
poolName: 'Payment Finance (PayFi) - Clearing Houses',
|
|
16
|
+
})).toBe('Senior');
|
|
17
|
+
});
|
|
18
|
+
it('never touches other tranche names, on any pool', () => {
|
|
19
|
+
expect(getTrancheDisplayName('Mezzanine', { poolName: 'Taxation Funding' })).toBe('Mezzanine');
|
|
20
|
+
expect(getTrancheDisplayName('Junior', { poolName: 'Taxation Funding' })).toBe('Junior');
|
|
21
|
+
expect(getTrancheDisplayName('Mezzanine', {
|
|
22
|
+
poolName: 'Payment Finance (PayFi)',
|
|
23
|
+
})).toBe('Mezzanine');
|
|
24
|
+
});
|
|
25
|
+
it('matches the Senior name case-insensitively but preserves unknown names verbatim', () => {
|
|
26
|
+
expect(getTrancheDisplayName('SENIOR', { poolName: 'Taxation Funding' })).toBe(UPPER_MEZZANINE);
|
|
27
|
+
expect(getTrancheDisplayName('Senior Plus', { poolName: 'Taxation Funding' })).toBe('Senior Plus');
|
|
28
|
+
});
|
|
29
|
+
it('treats missing pool names as Apxium (matches getCreditOriginator default)', () => {
|
|
30
|
+
// getCreditOriginator defaults unknown pools to Apxium; the display
|
|
31
|
+
// name must follow the same rule or the two surfaces would disagree.
|
|
32
|
+
expect(getTrancheDisplayName('Senior', { poolName: '' })).toBe(UPPER_MEZZANINE);
|
|
33
|
+
expect(getTrancheDisplayName('Senior', {})).toBe(UPPER_MEZZANINE);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
describe('getTrancheDisplayName — the widened PoolNameSignal', () => {
|
|
37
|
+
it('accepts the facade Strategy shape (`name`)', () => {
|
|
38
|
+
expect(getTrancheDisplayName('Senior', { name: 'Taxation Funding' })).toBe(UPPER_MEZZANINE);
|
|
39
|
+
expect(getTrancheDisplayName('Senior', {
|
|
40
|
+
name: 'Payment Finance (PayFi)',
|
|
41
|
+
})).toBe('Senior');
|
|
42
|
+
});
|
|
43
|
+
it('accepts a bare pool name', () => {
|
|
44
|
+
expect(getTrancheDisplayName('Senior', 'Whole Ledger Funding')).toBe(UPPER_MEZZANINE);
|
|
45
|
+
expect(getTrancheDisplayName('Senior', 'Payment Finance (PayFi)')).toBe('Senior');
|
|
46
|
+
});
|
|
47
|
+
it('gives the same answer for the raw and facade shapes of one pool', () => {
|
|
48
|
+
for (const poolName of [
|
|
49
|
+
'Taxation Funding (Tax Pay)',
|
|
50
|
+
'Payment Finance (PayFi)',
|
|
51
|
+
'Whole Ledger Funding',
|
|
52
|
+
'',
|
|
53
|
+
]) {
|
|
54
|
+
expect(getTrancheDisplayName('Senior', { poolName })).toBe(getTrancheDisplayName('Senior', { name: poolName }));
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
//# sourceMappingURL=tranche-display-name.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tranche-display-name.test.js","sourceRoot":"","sources":["../../src/domain/tranche-display-name.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { PoolOverview, TrancheData } from '../services/DataService/types';
|
|
2
|
+
/**
|
|
3
|
+
* Minimum remaining capacity (in stable-asset units) for a tranche to be
|
|
4
|
+
* selectable. Mirrors `MIN_CAPACITY = toBigNumber('1')` in kasu-fe-next's
|
|
5
|
+
* lending modal after its `formatEther`. Anything below this is rounding
|
|
6
|
+
* noise — the SDK's own capacity arithmetic routinely leaves sub-cent dust
|
|
7
|
+
* behind.
|
|
8
|
+
*/
|
|
9
|
+
export declare const MIN_TRANCHE_CAPACITY = 1;
|
|
10
|
+
/**
|
|
11
|
+
* Remaining capacity as it reaches this layer. `TrancheData` (raw, from the
|
|
12
|
+
* data service) calls it `poolCapacity`; the facade's `StrategyTranche` calls
|
|
13
|
+
* the same number `availableCapacity`. Accepting both keeps ONE capacity gate
|
|
14
|
+
* for the whole SDK rather than a duplicate per shape.
|
|
15
|
+
*/
|
|
16
|
+
export type TrancheCapacitySignal = {
|
|
17
|
+
poolCapacity: string;
|
|
18
|
+
} | {
|
|
19
|
+
availableCapacity: string;
|
|
20
|
+
};
|
|
21
|
+
export declare function trancheHasCapacity(tranche: TrancheCapacitySignal): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Whole-strategy capacity gate: true when the pool has tranches but none of
|
|
24
|
+
* them has remaining capacity. Drives a lend form's full-capacity message and
|
|
25
|
+
* its step-counter suppression.
|
|
26
|
+
*/
|
|
27
|
+
export declare function poolAllTranchesFull(pool: PoolOverview): boolean;
|
|
28
|
+
export type PoolStatus = 'Live' | 'Full' | 'Coming soon';
|
|
29
|
+
/**
|
|
30
|
+
* The status CODE for a pool. A code, not copy: consumers map it to their own
|
|
31
|
+
* wording and locale.
|
|
32
|
+
*/
|
|
33
|
+
export declare function derivePoolStatus(pool: PoolOverview): PoolStatus;
|
|
34
|
+
export declare function trancheRiskRank(tranche: TrancheData): number;
|
|
35
|
+
/**
|
|
36
|
+
* Comparator that orders tranches SAFEST-FIRST (Senior → Mezzanine → Junior)
|
|
37
|
+
* by the same name-based risk rank the default-pick uses. Ranks by the RAW
|
|
38
|
+
* subgraph name, so the Apxium "Upper Mezzanine" display rename never reaches
|
|
39
|
+
* here. Ties (incl. unrecognised names, which both rank last) keep their input
|
|
40
|
+
* order under a stable sort. Used by a lend dropdown to list the least-risky
|
|
41
|
+
* option first.
|
|
42
|
+
*/
|
|
43
|
+
export declare function compareTrancheSeniority(a: TrancheData, b: TrancheData): number;
|
|
44
|
+
/**
|
|
45
|
+
* Capacity-aware default tranche pick: the LOWEST-risk tranche that still has
|
|
46
|
+
* capacity, so a new lender lands on the safest available option. Riskier
|
|
47
|
+
* tranches remain a deliberate opt-in — we must never pre-select the
|
|
48
|
+
* highest-risk Junior (audit 3.2). When every tranche is full we fall back to
|
|
49
|
+
* the lowest-risk one regardless, so a form still mounts in a known state
|
|
50
|
+
* (the dropdown marks it full and the submit gate blocks progress).
|
|
51
|
+
*/
|
|
52
|
+
export declare function pickDefaultTrancheId(pool: PoolOverview): string;
|
|
53
|
+
/** A closed APY range, both bounds as 0..1 fractions. */
|
|
54
|
+
export interface ApyBounds {
|
|
55
|
+
min: number;
|
|
56
|
+
max: number;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* GROSS APY range across a pool's tranches — the numeric core of kasu-ui's
|
|
60
|
+
* `formatTrancheApyRange`. Each `TrancheData` carries `minApy`/`maxApy`
|
|
61
|
+
* derived from the base rate plus any fixed-term configs (`data-service.ts`).
|
|
62
|
+
*
|
|
63
|
+
* Zero and negative values are skipped — an unset/missing rate (e.g. a tranche
|
|
64
|
+
* with no fixed-term config) must not drag the range down to `0–10.5%`.
|
|
65
|
+
* `null` when nothing usable remains, which is the caller's cue to render its
|
|
66
|
+
* "no rate" state rather than a confident zero.
|
|
67
|
+
*
|
|
68
|
+
* The min is taken over `minApy` and the max over `maxApy`, so a single
|
|
69
|
+
* unusable side collapses the whole range to `null` rather than half a range.
|
|
70
|
+
*
|
|
71
|
+
* Formatting is NOT here. The rule that two bounds printing the same figure
|
|
72
|
+
* collapse to one is decided on the rendered digits, so it belongs with the
|
|
73
|
+
* formatter that produces them (kasu-ui `format-tranche-apy.ts`).
|
|
74
|
+
*/
|
|
75
|
+
export declare function trancheApyBounds(tranches: TrancheData[]): ApyBounds | null;
|
|
76
|
+
/**
|
|
77
|
+
* The same range as NET Effective Interest Rates — each bound through
|
|
78
|
+
* `netEffectiveApy`.
|
|
79
|
+
*
|
|
80
|
+
* FAIL CLOSED: `null` when either bound comes back non-finite or ≤ 0, so an
|
|
81
|
+
* out-of-domain fee or a rate-less pool can never be rendered as a confident
|
|
82
|
+
* figure. A caller that does not yet know the fee must not substitute `0` —
|
|
83
|
+
* a fee-less rate overstates the figure by up to 3.4pp with nothing on screen
|
|
84
|
+
* saying so; it should skip the call and render its unavailable state.
|
|
85
|
+
*
|
|
86
|
+
* @param feePercent the chain's performance fee, 0..100 — never a fraction.
|
|
87
|
+
* See `netEffectiveApy` for why the units matter.
|
|
88
|
+
*/
|
|
89
|
+
export declare function netTrancheApyBounds(tranches: TrancheData[], feePercent: number): ApyBounds | null;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { netEffectiveApy } from './rates';
|
|
2
|
+
/**
|
|
3
|
+
* Minimum remaining capacity (in stable-asset units) for a tranche to be
|
|
4
|
+
* selectable. Mirrors `MIN_CAPACITY = toBigNumber('1')` in kasu-fe-next's
|
|
5
|
+
* lending modal after its `formatEther`. Anything below this is rounding
|
|
6
|
+
* noise — the SDK's own capacity arithmetic routinely leaves sub-cent dust
|
|
7
|
+
* behind.
|
|
8
|
+
*/
|
|
9
|
+
export const MIN_TRANCHE_CAPACITY = 1;
|
|
10
|
+
export function trancheHasCapacity(tranche) {
|
|
11
|
+
const raw = 'poolCapacity' in tranche
|
|
12
|
+
? tranche.poolCapacity
|
|
13
|
+
: tranche.availableCapacity;
|
|
14
|
+
const remaining = parseFloat(raw);
|
|
15
|
+
return Number.isFinite(remaining) && remaining >= MIN_TRANCHE_CAPACITY;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Whole-strategy capacity gate: true when the pool has tranches but none of
|
|
19
|
+
* them has remaining capacity. Drives a lend form's full-capacity message and
|
|
20
|
+
* its step-counter suppression.
|
|
21
|
+
*/
|
|
22
|
+
export function poolAllTranchesFull(pool) {
|
|
23
|
+
return pool.tranches.length > 0 && !pool.tranches.some(trancheHasCapacity);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The status CODE for a pool. A code, not copy: consumers map it to their own
|
|
27
|
+
* wording and locale.
|
|
28
|
+
*/
|
|
29
|
+
export function derivePoolStatus(pool) {
|
|
30
|
+
if (!pool.enabled)
|
|
31
|
+
return 'Coming soon';
|
|
32
|
+
if (poolAllTranchesFull(pool))
|
|
33
|
+
return 'Full';
|
|
34
|
+
return 'Live';
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Seniority rank of a tranche by name — LOWER is less risky. The credit
|
|
38
|
+
* waterfall is Senior (repaid first, lowest risk) → Mezzanine → Junior
|
|
39
|
+
* (repaid last, highest risk). APY is NOT a reliable proxy (a pool can price
|
|
40
|
+
* its Senior tranche above its Mezzanine), so risk is ranked by name.
|
|
41
|
+
* Unrecognised names sort last, so a known-safe tranche always wins the
|
|
42
|
+
* default. Ranks by the RAW subgraph name — the Apxium "Upper Mezzanine"
|
|
43
|
+
* rename is display-only and must never reach here (see
|
|
44
|
+
* `tranche-display-name.ts`).
|
|
45
|
+
*/
|
|
46
|
+
const TRANCHE_RISK_RANK = {
|
|
47
|
+
senior: 0,
|
|
48
|
+
mezzanine: 1,
|
|
49
|
+
junior: 2,
|
|
50
|
+
};
|
|
51
|
+
// `name` is typed `string` on `TrancheData` but arrives from the subgraph, so
|
|
52
|
+
// it is taken as possibly missing here: an unnamed tranche ranks last rather
|
|
53
|
+
// than throwing on `.trim()`.
|
|
54
|
+
function riskRankOfName(name) {
|
|
55
|
+
var _a, _b;
|
|
56
|
+
return ((_b = TRANCHE_RISK_RANK[(_a = name === null || name === void 0 ? void 0 : name.trim().toLowerCase()) !== null && _a !== void 0 ? _a : '']) !== null && _b !== void 0 ? _b : Number.POSITIVE_INFINITY);
|
|
57
|
+
}
|
|
58
|
+
export function trancheRiskRank(tranche) {
|
|
59
|
+
return riskRankOfName(tranche.name);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Comparator that orders tranches SAFEST-FIRST (Senior → Mezzanine → Junior)
|
|
63
|
+
* by the same name-based risk rank the default-pick uses. Ranks by the RAW
|
|
64
|
+
* subgraph name, so the Apxium "Upper Mezzanine" display rename never reaches
|
|
65
|
+
* here. Ties (incl. unrecognised names, which both rank last) keep their input
|
|
66
|
+
* order under a stable sort. Used by a lend dropdown to list the least-risky
|
|
67
|
+
* option first.
|
|
68
|
+
*/
|
|
69
|
+
export function compareTrancheSeniority(a, b) {
|
|
70
|
+
const ra = trancheRiskRank(a);
|
|
71
|
+
const rb = trancheRiskRank(b);
|
|
72
|
+
if (ra === rb)
|
|
73
|
+
return 0;
|
|
74
|
+
return ra < rb ? -1 : 1;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Capacity-aware default tranche pick: the LOWEST-risk tranche that still has
|
|
78
|
+
* capacity, so a new lender lands on the safest available option. Riskier
|
|
79
|
+
* tranches remain a deliberate opt-in — we must never pre-select the
|
|
80
|
+
* highest-risk Junior (audit 3.2). When every tranche is full we fall back to
|
|
81
|
+
* the lowest-risk one regardless, so a form still mounts in a known state
|
|
82
|
+
* (the dropdown marks it full and the submit gate blocks progress).
|
|
83
|
+
*/
|
|
84
|
+
export function pickDefaultTrancheId(pool) {
|
|
85
|
+
const withCapacity = pool.tranches.filter(trancheHasCapacity);
|
|
86
|
+
const candidates = withCapacity.length > 0 ? withCapacity : pool.tranches;
|
|
87
|
+
if (candidates.length === 0)
|
|
88
|
+
return '';
|
|
89
|
+
return candidates.reduce((best, t) => trancheRiskRank(t) < trancheRiskRank(best) ? t : best).id;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* GROSS APY range across a pool's tranches — the numeric core of kasu-ui's
|
|
93
|
+
* `formatTrancheApyRange`. Each `TrancheData` carries `minApy`/`maxApy`
|
|
94
|
+
* derived from the base rate plus any fixed-term configs (`data-service.ts`).
|
|
95
|
+
*
|
|
96
|
+
* Zero and negative values are skipped — an unset/missing rate (e.g. a tranche
|
|
97
|
+
* with no fixed-term config) must not drag the range down to `0–10.5%`.
|
|
98
|
+
* `null` when nothing usable remains, which is the caller's cue to render its
|
|
99
|
+
* "no rate" state rather than a confident zero.
|
|
100
|
+
*
|
|
101
|
+
* The min is taken over `minApy` and the max over `maxApy`, so a single
|
|
102
|
+
* unusable side collapses the whole range to `null` rather than half a range.
|
|
103
|
+
*
|
|
104
|
+
* Formatting is NOT here. The rule that two bounds printing the same figure
|
|
105
|
+
* collapse to one is decided on the rendered digits, so it belongs with the
|
|
106
|
+
* formatter that produces them (kasu-ui `format-tranche-apy.ts`).
|
|
107
|
+
*/
|
|
108
|
+
export function trancheApyBounds(tranches) {
|
|
109
|
+
if (tranches.length === 0)
|
|
110
|
+
return null;
|
|
111
|
+
let lo = Number.POSITIVE_INFINITY;
|
|
112
|
+
let hi = Number.NEGATIVE_INFINITY;
|
|
113
|
+
for (const t of tranches) {
|
|
114
|
+
const min = parseFloat(t.minApy);
|
|
115
|
+
const max = parseFloat(t.maxApy);
|
|
116
|
+
if (Number.isFinite(min) && min > 0)
|
|
117
|
+
lo = Math.min(lo, min);
|
|
118
|
+
if (Number.isFinite(max) && max > 0)
|
|
119
|
+
hi = Math.max(hi, max);
|
|
120
|
+
}
|
|
121
|
+
if (!Number.isFinite(lo) || !Number.isFinite(hi))
|
|
122
|
+
return null;
|
|
123
|
+
return { min: lo, max: hi };
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* The same range as NET Effective Interest Rates — each bound through
|
|
127
|
+
* `netEffectiveApy`.
|
|
128
|
+
*
|
|
129
|
+
* FAIL CLOSED: `null` when either bound comes back non-finite or ≤ 0, so an
|
|
130
|
+
* out-of-domain fee or a rate-less pool can never be rendered as a confident
|
|
131
|
+
* figure. A caller that does not yet know the fee must not substitute `0` —
|
|
132
|
+
* a fee-less rate overstates the figure by up to 3.4pp with nothing on screen
|
|
133
|
+
* saying so; it should skip the call and render its unavailable state.
|
|
134
|
+
*
|
|
135
|
+
* @param feePercent the chain's performance fee, 0..100 — never a fraction.
|
|
136
|
+
* See `netEffectiveApy` for why the units matter.
|
|
137
|
+
*/
|
|
138
|
+
export function netTrancheApyBounds(tranches, feePercent) {
|
|
139
|
+
const gross = trancheApyBounds(tranches);
|
|
140
|
+
if (!gross)
|
|
141
|
+
return null;
|
|
142
|
+
const min = netEffectiveApy(gross.min, feePercent);
|
|
143
|
+
const max = netEffectiveApy(gross.max, feePercent);
|
|
144
|
+
if (!Number.isFinite(min) || min <= 0)
|
|
145
|
+
return null;
|
|
146
|
+
if (!Number.isFinite(max) || max <= 0)
|
|
147
|
+
return null;
|
|
148
|
+
return { min, max };
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=tranches.js.map
|