@kasufinance/kasu-sdk 2.4.2 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +189 -253
  3. package/dist/bundle.cjs.js +4775 -1794
  4. package/dist/bundle.esm.js +4764 -1816
  5. package/dist/domain/deposit-bounds.d.ts +62 -0
  6. package/dist/domain/deposit-bounds.js +84 -0
  7. package/dist/domain/deposit-bounds.js.map +1 -0
  8. package/dist/domain/deposit-bounds.test.js +135 -0
  9. package/dist/domain/deposit-bounds.test.js.map +1 -0
  10. package/dist/domain/index.d.ts +26 -0
  11. package/dist/domain/index.js +23 -0
  12. package/dist/domain/index.js.map +1 -0
  13. package/dist/domain/partners.d.ts +34 -0
  14. package/dist/domain/partners.js +43 -0
  15. package/dist/domain/partners.js.map +1 -0
  16. package/dist/domain/partners.test.d.ts +1 -0
  17. package/dist/domain/partners.test.js +53 -0
  18. package/dist/domain/partners.test.js.map +1 -0
  19. package/dist/domain/pools.d.ts +70 -0
  20. package/dist/domain/pools.js +134 -0
  21. package/dist/domain/pools.js.map +1 -0
  22. package/dist/domain/pools.test.d.ts +1 -0
  23. package/dist/domain/pools.test.js +184 -0
  24. package/dist/domain/pools.test.js.map +1 -0
  25. package/dist/domain/rates.d.ts +83 -0
  26. package/dist/domain/rates.js +120 -0
  27. package/dist/domain/rates.js.map +1 -0
  28. package/dist/domain/rates.test.d.ts +1 -0
  29. package/dist/domain/rates.test.js +181 -0
  30. package/dist/domain/rates.test.js.map +1 -0
  31. package/dist/domain/tranche-display-name.d.ts +17 -0
  32. package/dist/domain/tranche-display-name.js +30 -0
  33. package/dist/domain/tranche-display-name.js.map +1 -0
  34. package/dist/domain/tranche-display-name.test.d.ts +1 -0
  35. package/dist/domain/tranche-display-name.test.js +58 -0
  36. package/dist/domain/tranche-display-name.test.js.map +1 -0
  37. package/dist/domain/tranches.d.ts +89 -0
  38. package/dist/domain/tranches.js +150 -0
  39. package/dist/domain/tranches.js.map +1 -0
  40. package/dist/domain/tranches.test.d.ts +1 -0
  41. package/dist/domain/tranches.test.js +206 -0
  42. package/dist/domain/tranches.test.js.map +1 -0
  43. package/dist/facade/chain-configs.js +54 -0
  44. package/dist/facade/chain-configs.js.map +1 -1
  45. package/dist/facade/config.test.d.ts +1 -0
  46. package/dist/facade/config.test.js +216 -0
  47. package/dist/facade/config.test.js.map +1 -0
  48. package/dist/facade/deposits.d.ts +22 -1
  49. package/dist/facade/deposits.js +24 -1
  50. package/dist/facade/deposits.js.map +1 -1
  51. package/dist/facade/facade.test.d.ts +1 -0
  52. package/dist/{tests → facade}/facade.test.js +10 -5
  53. package/dist/facade/facade.test.js.map +1 -0
  54. package/dist/facade/index.d.ts +2 -1
  55. package/dist/facade/index.js +2 -0
  56. package/dist/facade/index.js.map +1 -1
  57. package/dist/facade/kasu.d.ts +49 -8
  58. package/dist/facade/kasu.js +117 -13
  59. package/dist/facade/kasu.js.map +1 -1
  60. package/dist/facade/strategies.d.ts +25 -0
  61. package/dist/facade/strategies.js +36 -0
  62. package/dist/facade/strategies.js.map +1 -1
  63. package/dist/facade/types.d.ts +57 -2
  64. package/dist/facade/unused-pool-ids.d.ts +31 -0
  65. package/dist/facade/unused-pool-ids.js +57 -0
  66. package/dist/facade/unused-pool-ids.js.map +1 -0
  67. package/dist/index.d.ts +1 -0
  68. package/dist/index.js +5 -0
  69. package/dist/index.js.map +1 -1
  70. package/dist/sdk-config.d.ts +11 -0
  71. package/dist/sdk-config.js +8 -1
  72. package/dist/sdk-config.js.map +1 -1
  73. package/dist/services/DataService/data-service.d.ts +5 -0
  74. package/dist/services/DataService/data-service.js +7 -2
  75. package/dist/services/DataService/data-service.js.map +1 -1
  76. package/dist/services/Locking/calculate-apy.test.d.ts +1 -0
  77. package/dist/services/Locking/calculate-apy.test.js +41 -0
  78. package/dist/services/Locking/calculate-apy.test.js.map +1 -0
  79. package/dist/services/Locking/locking.d.ts +14 -0
  80. package/dist/services/Locking/locking.js +16 -2
  81. package/dist/services/Locking/locking.js.map +1 -1
  82. package/dist/tests/sample.test.js +46 -25
  83. package/dist/tests/sample.test.js.map +1 -1
  84. package/package.json +43 -12
  85. package/src/domain/deposit-bounds.test.ts +200 -0
  86. package/src/domain/deposit-bounds.ts +118 -0
  87. package/src/domain/index.ts +70 -0
  88. package/src/domain/partners.test.ts +83 -0
  89. package/src/domain/partners.ts +66 -0
  90. package/src/domain/pools.test.ts +260 -0
  91. package/src/domain/pools.ts +150 -0
  92. package/src/domain/rates.test.ts +254 -0
  93. package/src/domain/rates.ts +120 -0
  94. package/src/domain/tranche-display-name.test.ts +96 -0
  95. package/src/domain/tranche-display-name.ts +34 -0
  96. package/src/domain/tranches.test.ts +300 -0
  97. package/src/domain/tranches.ts +177 -0
  98. package/src/facade/chain-configs.ts +55 -0
  99. package/src/facade/config.test.ts +265 -0
  100. package/src/facade/deposits.ts +25 -0
  101. package/src/{tests → facade}/facade.test.ts +10 -5
  102. package/src/facade/index.ts +4 -0
  103. package/src/facade/kasu.ts +155 -12
  104. package/src/facade/strategies.ts +36 -0
  105. package/src/facade/types.ts +60 -2
  106. package/src/facade/unused-pool-ids.ts +59 -0
  107. package/src/index.ts +6 -0
  108. package/src/sdk-config.ts +19 -1
  109. package/src/services/DataService/data-service.ts +7 -2
  110. package/src/services/Locking/calculate-apy.test.ts +52 -0
  111. package/src/services/Locking/locking.ts +16 -2
  112. package/src/tests/sample.test.ts +54 -33
  113. package/.eslintignore +0 -3
  114. package/.eslintrc.json +0 -90
  115. package/.github/workflows/ci.yml +0 -39
  116. package/.github/workflows/gitleaks.yml +0 -22
  117. package/.github/workflows/main.yml +0 -41
  118. package/.gitleaksignore +0 -19
  119. package/.prettierignore +0 -3
  120. package/.prettierrc +0 -28
  121. package/CLAUDE.md +0 -410
  122. package/copy-abi.ps1 +0 -15
  123. package/dist/tests/facade.test.js.map +0 -1
  124. package/index.ts +0 -2
  125. package/jest-config.ts +0 -9
  126. package/jest.config.js +0 -8
  127. package/rollup.config.js +0 -45
  128. package/tsconfig.json +0 -27
  129. /package/dist/{tests/facade.test.d.ts → domain/deposit-bounds.test.d.ts} +0 -0
@@ -0,0 +1,134 @@
1
+ import { netEffectiveApy } from './rates';
2
+ import { trancheHasCapacity } from './tranches';
3
+ /**
4
+ * The highest FTD-inclusive upper bound across a pool's tranches, or `0` when
5
+ * none is usable. The sort key behind `selectVisiblePools`.
6
+ */
7
+ export function poolMaxApy(pool) {
8
+ let hi = 0;
9
+ for (const t of pool.tranches) {
10
+ const v = parseFloat(t.maxApy);
11
+ if (Number.isFinite(v) && v > hi)
12
+ hi = v;
13
+ }
14
+ return hi;
15
+ }
16
+ /**
17
+ * A lending grid's visible pool set: *active* and *not yet oversubscribed*
18
+ * pools, sorted with has-capacity strategies first, then by highest max APY.
19
+ *
20
+ * Shared so a server-rendered payload and a client re-fetch can never
21
+ * disagree — kasu-ui's SSR fetcher and client hook used to carry verbatim
22
+ * copies of this filter + sort, and any drift surfaced as an SSR→CSR content
23
+ * flip on the lending page.
24
+ *
25
+ * Pure: operates on a fresh array (`filter` output), so the in-place `sort`
26
+ * does not mutate the caller's input.
27
+ */
28
+ export function selectVisiblePools(all) {
29
+ const active = all.filter((p) => p.isActive && !p.isOversubscribed);
30
+ return active.sort((a, b) => {
31
+ const aHas = a.tranches.some(trancheHasCapacity) ? 1 : 0;
32
+ const bHas = b.tranches.some(trancheHasCapacity) ? 1 : 0;
33
+ if (aHas !== bHas)
34
+ return bHas - aHas;
35
+ return poolMaxApy(b) - poolMaxApy(a);
36
+ });
37
+ }
38
+ // `tranches` is typed as a required array but arrives from the subgraph, so
39
+ // both scanners below read it as possibly missing — a pool with no tranche
40
+ // list contributes nothing instead of throwing. Mirrors the `?? []` the
41
+ // kasu-ui originals carried.
42
+ function tranchesOf(tranches) {
43
+ return tranches !== null && tranches !== void 0 ? tranches : [];
44
+ }
45
+ /**
46
+ * The highest-yielding tranche that still has capacity, across all pools.
47
+ *
48
+ * Compares `tranche.apy` — the base variable rate — NOT `minApy`/`maxApy`,
49
+ * which the data service widens with fixed-term-deposit variants. A headline
50
+ * naming one tranche should advertise the rate every depositor gets, not an
51
+ * FTD-locked uplift.
52
+ *
53
+ * Capacity is gated by the shared `trancheHasCapacity` (≥ 1 stable unit), so
54
+ * full tranches never win. Ties on APY break toward more remaining capacity.
55
+ * Returns `null` when no tranche qualifies (empty list, all full, or all rates
56
+ * unset) — callers fall back to static copy.
57
+ */
58
+ export function pickHighestYieldTranche(pools) {
59
+ if (!(pools === null || pools === void 0 ? void 0 : pools.length))
60
+ return null;
61
+ let best = null;
62
+ for (const pool of pools) {
63
+ for (const tranche of tranchesOf(pool.tranches)) {
64
+ if (!trancheHasCapacity(tranche))
65
+ continue;
66
+ const apy = parseFloat(tranche.apy);
67
+ if (!Number.isFinite(apy) || apy <= 0)
68
+ continue;
69
+ if (!best) {
70
+ best = { pool, tranche, apy };
71
+ continue;
72
+ }
73
+ const capacity = parseFloat(tranche.poolCapacity);
74
+ const bestCapacity = parseFloat(best.tranche.poolCapacity);
75
+ if (apy > best.apy || (apy === best.apy && capacity > bestCapacity)) {
76
+ best = { pool, tranche, apy };
77
+ }
78
+ }
79
+ }
80
+ return best;
81
+ }
82
+ /**
83
+ * The highest NET Effective Interest Rate on offer across the pools passed in
84
+ * — the ceiling behind an "up to …" claim. The numeric core of kasu-ui's
85
+ * `formatMaxNetRate`.
86
+ *
87
+ * `null` when there is nothing to quote, and callers must then DROP the claim
88
+ * rather than print a placeholder: this feeds text that gets posted publicly.
89
+ *
90
+ * Scope follows the caller's pool list, which is normally keyed to one chain —
91
+ * so a lender on XDC AUDD quotes the AUDD ceiling, not Base's. That is the
92
+ * intended reading: they are sharing the deployment they are on.
93
+ *
94
+ * Reads `maxApy` — the FTD-inclusive upper bound — unlike
95
+ * `pickHighestYieldTranche` above, which names one tranche's variable rate. A
96
+ * ceiling claim and a specific tranche's rate are different statements, and
97
+ * the ceiling must agree with the top of `netTrancheApyBounds`, which reads
98
+ * the same field. `apy` is taken into the comparison too: `maxApy` is derived
99
+ * from the base rate plus any fixed-term configs, so it can never be lower,
100
+ * and a tranche that carries no `maxApy` at all still contributes its base
101
+ * rate instead of dropping out.
102
+ *
103
+ * Capacity-gated by `trancheHasCapacity` — we never advertise a rate no lender
104
+ * could take up.
105
+ *
106
+ * FAIL CLOSED: an out-of-domain fee, or a net rate that is non-finite or ≤ 0,
107
+ * returns `null`. The gross figure is never a substitute, and a caller that
108
+ * does not yet know the fee must not pass `0`.
109
+ *
110
+ * @param feePercent the chain's performance fee, 0..100 — never a fraction.
111
+ */
112
+ export function maxNetRateCeiling(pools, feePercent) {
113
+ if (!(pools === null || pools === void 0 ? void 0 : pools.length))
114
+ return null;
115
+ let ceiling = 0;
116
+ for (const pool of pools) {
117
+ for (const tranche of tranchesOf(pool.tranches)) {
118
+ if (!trancheHasCapacity(tranche))
119
+ continue;
120
+ for (const raw of [tranche.maxApy, tranche.apy]) {
121
+ const apy = parseFloat(raw);
122
+ if (Number.isFinite(apy) && apy > ceiling)
123
+ ceiling = apy;
124
+ }
125
+ }
126
+ }
127
+ if (ceiling <= 0)
128
+ return null;
129
+ const rate = netEffectiveApy(ceiling, feePercent);
130
+ if (!Number.isFinite(rate) || rate <= 0)
131
+ return null;
132
+ return rate;
133
+ }
134
+ //# sourceMappingURL=pools.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pools.js","sourceRoot":"","sources":["../../src/domain/pools.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,184 @@
1
+ import { maxNetRateCeiling, pickHighestYieldTranche, poolMaxApy, selectVisiblePools, } from './pools';
2
+ import { netEffectiveApy } from './rates';
3
+ /**
4
+ * Ported from kasu-ui `select-visible-pools.test.ts` and the
5
+ * `pickHighestYieldTranche` / `formatMaxNetRate` cases of
6
+ * `pick-best-tranche.test.ts`. The `formatMaxNetRate` assertions are on the
7
+ * ceiling NUMBER here; their rendered strings stay in kasu-ui.
8
+ */
9
+ function tranche(name, apy, poolCapacity, maxApy) {
10
+ return { id: name, name, apy, poolCapacity, maxApy };
11
+ }
12
+ function pool(over) {
13
+ return Object.assign({ id: 'p', poolName: 'p', isActive: true, isOversubscribed: false, tranches: [tranche('Senior', '0.09', '250000', '0.10')] }, over);
14
+ }
15
+ const withCapacity = () => [
16
+ tranche('Senior', '0.09', '250000', '0.10'),
17
+ ];
18
+ const noCapacity = () => [tranche('Senior', '0.09', '0', '0.10')];
19
+ /** The live performance fee on all deployments. */
20
+ const FEE = 10;
21
+ describe('poolMaxApy', () => {
22
+ it('takes the highest usable maxApy across tranches', () => {
23
+ expect(poolMaxApy(pool({
24
+ tranches: [
25
+ tranche('Senior', '0.09', '1', '0.10'),
26
+ tranche('Junior', '0.12', '1', '0.18'),
27
+ ],
28
+ }))).toBe(0.18);
29
+ });
30
+ it('is 0 when nothing is usable', () => {
31
+ expect(poolMaxApy(pool({ tranches: [] }))).toBe(0);
32
+ expect(poolMaxApy(pool({ tranches: [tranche('Senior', '0.09', '1', 'x')] }))).toBe(0);
33
+ });
34
+ });
35
+ describe('selectVisiblePools', () => {
36
+ it('drops inactive pools', () => {
37
+ const pools = [
38
+ pool({ id: 'active', isActive: true }),
39
+ pool({ id: 'inactive', isActive: false }),
40
+ ];
41
+ expect(selectVisiblePools(pools).map((p) => p.id)).toEqual(['active']);
42
+ });
43
+ it('drops oversubscribed pools', () => {
44
+ const pools = [
45
+ pool({ id: 'open', isOversubscribed: false }),
46
+ pool({ id: 'full', isOversubscribed: true }),
47
+ ];
48
+ expect(selectVisiblePools(pools).map((p) => p.id)).toEqual(['open']);
49
+ });
50
+ it('sorts pools with capacity ahead of pools without', () => {
51
+ const pools = [
52
+ pool({ id: 'empty', tranches: noCapacity() }),
53
+ pool({ id: 'has-cap', tranches: withCapacity() }),
54
+ ];
55
+ expect(selectVisiblePools(pools).map((p) => p.id)).toEqual([
56
+ 'has-cap',
57
+ 'empty',
58
+ ]);
59
+ });
60
+ it('sorts by highest max APY within the same capacity bucket', () => {
61
+ const pools = [
62
+ pool({
63
+ id: 'low',
64
+ tranches: [tranche('Senior', '0.08', '250000', '0.08')],
65
+ }),
66
+ pool({
67
+ id: 'high',
68
+ tranches: [tranche('Senior', '0.15', '250000', '0.15')],
69
+ }),
70
+ ];
71
+ expect(selectVisiblePools(pools).map((p) => p.id)).toEqual([
72
+ 'high',
73
+ 'low',
74
+ ]);
75
+ });
76
+ it('does not mutate the input array', () => {
77
+ const pools = [
78
+ pool({ id: 'a', tranches: noCapacity() }),
79
+ pool({ id: 'b', tranches: withCapacity() }),
80
+ ];
81
+ const before = pools.map((p) => p.id);
82
+ selectVisiblePools(pools);
83
+ expect(pools.map((p) => p.id)).toEqual(before);
84
+ });
85
+ });
86
+ describe('pickHighestYieldTranche', () => {
87
+ const named = (poolName, tranches) => pool({ id: poolName, poolName, tranches });
88
+ it('returns null for empty / nullish input', () => {
89
+ expect(pickHighestYieldTranche(undefined)).toBeNull();
90
+ expect(pickHighestYieldTranche(null)).toBeNull();
91
+ expect(pickHighestYieldTranche([])).toBeNull();
92
+ });
93
+ it('picks the highest base APY across all pools with capacity', () => {
94
+ const best = pickHighestYieldTranche([
95
+ named('A', [tranche('Junior', '0.12', '5000')]),
96
+ named('B', [
97
+ tranche('Senior', '0.18', '5000'),
98
+ tranche('Mezz', '0.15', '5000'),
99
+ ]),
100
+ ]);
101
+ expect(best === null || best === void 0 ? void 0 : best.pool.poolName).toBe('B');
102
+ expect(best === null || best === void 0 ? void 0 : best.tranche.name).toBe('Senior');
103
+ expect(best === null || best === void 0 ? void 0 : best.apy).toBeCloseTo(0.18);
104
+ });
105
+ it('skips tranches without capacity (< 1 unit)', () => {
106
+ const best = pickHighestYieldTranche([
107
+ named('Full', [tranche('Junior', '0.30', '0')]),
108
+ named('Live', [tranche('Senior', '0.10', '2000')]),
109
+ ]);
110
+ expect(best === null || best === void 0 ? void 0 : best.pool.poolName).toBe('Live');
111
+ expect(best === null || best === void 0 ? void 0 : best.apy).toBeCloseTo(0.1);
112
+ });
113
+ it('breaks APY ties toward more remaining capacity', () => {
114
+ const best = pickHighestYieldTranche([
115
+ named('Small', [tranche('Junior', '0.15', '1000')]),
116
+ named('Big', [tranche('Junior', '0.15', '9000')]),
117
+ ]);
118
+ expect(best === null || best === void 0 ? void 0 : best.pool.poolName).toBe('Big');
119
+ });
120
+ it('ignores zero / non-finite APY values', () => {
121
+ const best = pickHighestYieldTranche([
122
+ named('Zero', [tranche('Junior', '0', '5000')]),
123
+ named('NaN', [tranche('Junior', 'not-a-number', '5000')]),
124
+ named('Real', [tranche('Junior', '0.09', '5000')]),
125
+ ]);
126
+ expect(best === null || best === void 0 ? void 0 : best.pool.poolName).toBe('Real');
127
+ });
128
+ it('reads the BASE rate, not the FTD-widened maxApy', () => {
129
+ // A ceiling claim and a named tranche's rate are different statements.
130
+ const best = pickHighestYieldTranche([
131
+ named('A', [tranche('Junior', '0.12', '5000', '0.30')]),
132
+ named('B', [tranche('Senior', '0.18', '5000', '0.18')]),
133
+ ]);
134
+ expect(best === null || best === void 0 ? void 0 : best.pool.poolName).toBe('B');
135
+ });
136
+ });
137
+ describe('maxNetRateCeiling', () => {
138
+ const named = (poolName, tranches) => pool({ id: poolName, poolName, tranches });
139
+ it('returns the highest ceiling across all pools, net of the fee', () => {
140
+ const rate = maxNetRateCeiling([
141
+ named('A', [tranche('Junior', '0.12', '5000', '0.18')]),
142
+ named('B', [tranche('Senior', '0.10', '5000', '0.22')]),
143
+ ], FEE);
144
+ // 0.22 gross → 19.60% net, the same arithmetic as everywhere else.
145
+ expect(rate).toBe(netEffectiveApy(0.22, FEE));
146
+ expect(rate).toBeCloseTo(0.196, 4);
147
+ });
148
+ it('prefers the FTD-inclusive maxApy over the base rate', () => {
149
+ // The strategies card's range tops out at maxApy; an "up to" claim
150
+ // quoting the base rate would contradict it on the same screen.
151
+ expect(maxNetRateCeiling([named('A', [tranche('Junior', '0.10', '5000', '0.22')])], FEE)).toBe(netEffectiveApy(0.22, FEE));
152
+ });
153
+ it('falls back to the base rate when a tranche carries no maxApy', () => {
154
+ expect(maxNetRateCeiling([named('A', [tranche('Junior', '0.22', '5000')])], FEE)).toBe(netEffectiveApy(0.22, FEE));
155
+ });
156
+ it('skips tranches without capacity — an unreachable rate is not a claim', () => {
157
+ const rate = maxNetRateCeiling([
158
+ named('Full', [tranche('Junior', '0.30', '0', '0.30')]),
159
+ named('Live', [tranche('Senior', '0.10', '5000', '0.10')]),
160
+ ], FEE);
161
+ expect(rate).toBe(netEffectiveApy(0.1, FEE));
162
+ expect(rate).toBeCloseTo(0.0896, 4);
163
+ });
164
+ it('returns null rather than a gross figure when the fee is out of domain', () => {
165
+ const pools = [named('A', [tranche('Junior', '0.22', '5000', '0.22')])];
166
+ expect(maxNetRateCeiling(pools, 1000)).toBeNull();
167
+ expect(maxNetRateCeiling(pools, -1)).toBeNull();
168
+ expect(maxNetRateCeiling(pools, NaN)).toBeNull();
169
+ // A 100% fee leaves nothing to claim.
170
+ expect(maxNetRateCeiling(pools, 100)).toBeNull();
171
+ });
172
+ it('returns null for empty, nullish or rate-less input', () => {
173
+ expect(maxNetRateCeiling(undefined, FEE)).toBeNull();
174
+ expect(maxNetRateCeiling(null, FEE)).toBeNull();
175
+ expect(maxNetRateCeiling([], FEE)).toBeNull();
176
+ expect(maxNetRateCeiling([named('Zero', [tranche('Junior', '0', '5000', '0')])], FEE)).toBeNull();
177
+ });
178
+ it('agrees with the top of the tranche range it must sit alongside', () => {
179
+ // Both read maxApy through netEffectiveApy; they must not disagree.
180
+ const tranches = [tranche('Senior', '0.14', '5000', '0.22')];
181
+ expect(maxNetRateCeiling([named('A', tranches)], FEE)).toBe(netEffectiveApy(0.22, FEE));
182
+ });
183
+ });
184
+ //# sourceMappingURL=pools.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pools.test.js","sourceRoot":"","sources":["../../src/domain/pools.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Epochs in a year — the single source of truth for every rate conversion in
3
+ * the SDK and in every consumer that undoes one.
4
+ *
5
+ * The subgraph carries a per-epoch `interestRate`; `epochRateToApy` compounds
6
+ * it into an APY and `apyToEpochRate` inverts exactly that. A second copy of
7
+ * this number anywhere would recover a different weekly rate and every derived
8
+ * figure — displayed APYs, net rates, projected interest — would be wrong, so
9
+ * `DataService.calculateApyForTranche` and `KSULocking.calculateApy` both read
10
+ * it from here rather than declaring their own.
11
+ */
12
+ export declare const EPOCHS_IN_YEAR = 52.17857;
13
+ /**
14
+ * A per-epoch interest rate compounded into an annual rate.
15
+ *
16
+ * ```
17
+ * apy = (1 + r)^E − 1
18
+ * ```
19
+ *
20
+ * This is the exact expression `DataService.calculateApyForTranche` has always
21
+ * used, kept in its literal `**` form rather than rewritten through
22
+ * `expm1`/`log1p`: it is the SDK's OWN definition of `tranche.apy`, so its
23
+ * float behaviour is part of the contract every consumer already calibrated
24
+ * against. `apyToEpochRate` is the one that must be numerically careful,
25
+ * because it runs on the result.
26
+ *
27
+ * @param epochRate the per-epoch rate as a 0..1 fraction (e.g. `0.003`).
28
+ * @returns the compounded annual rate as a 0..1 fraction.
29
+ */
30
+ export declare function epochRateToApy(epochRate: number): number;
31
+ /**
32
+ * The inverse of `epochRateToApy` — the per-epoch rate an APY was compounded
33
+ * from.
34
+ *
35
+ * `expm1`/`log1p` rather than the literal `(1 + apy) ** (1 / E) - 1`.
36
+ * Algebraically identical, but the direct form computes a double just above 1
37
+ * and subtracts 1 from it, discarding the low bits of a result that is itself
38
+ * ~1e-3 for a realistic rate. These two never form the intermediate near-1
39
+ * value.
40
+ *
41
+ * @param apy the compounded annual rate as a 0..1 fraction.
42
+ * @returns the per-epoch rate as a 0..1 fraction.
43
+ */
44
+ export declare function apyToEpochRate(apy: number): number;
45
+ /**
46
+ * The NET Effective Interest Rate — the compounded annual rate a lender
47
+ * actually earns, after the platform performance fee.
48
+ *
49
+ * ```
50
+ * r = (1 + grossApy)^(1/E) − 1 // the gross per-epoch rate
51
+ * net = (1 + r · (1 − feePercent/100))^E − 1 // fee taken each epoch, then compounded
52
+ * ```
53
+ *
54
+ * `LendingPool._applyTrancheInterest` mints the lender
55
+ * `interest × (1 − performanceFee)` at every epoch close, and it is that NET
56
+ * amount which capitalises and earns interest in the following epoch. So the
57
+ * fee is applied per epoch and the result is re-compounded — not deducted from
58
+ * the annual figure.
59
+ *
60
+ * ⚠️ UNITS — the one thing that must not be got wrong.
61
+ * `feePercent` is a PERCENTAGE IN 0..100, **not** a 0..1 fraction.
62
+ * `DataService.getPerformanceFee()` (and `StrategiesFacade.getPerformanceFeePercent()`)
63
+ * returns the subgraph's `integerToPercentage2(1000)` = `10`, meaning **ten
64
+ * percent**. It is NOT `0.10`. Treating it as a fraction computes
65
+ * `r · (1 − 10)` = `−9r` and yields a nonsense negative rate that would render
66
+ * as a plausible-looking `-…%`, which is why `feePercent > 100` and
67
+ * `feePercent < 0` both return `NaN` and why the parameter is named
68
+ * `feePercent` and never `fee`.
69
+ *
70
+ * This returns a NUMBER and nothing else. Rendering it — the 2dp convention,
71
+ * the ` p.a.` suffix, and the fail-closed em-dash for a `NaN` or non-positive
72
+ * result — is the consumer's job: kasu-ui does it in
73
+ * `src/features/lending/lib/interest-rate.ts` (`formatEffectiveRate`,
74
+ * `RATE_UNAVAILABLE`), which now wraps this function instead of restating it.
75
+ *
76
+ * @param grossApy compounded GROSS APY as a 0..1 fraction (e.g. `0.22` for
77
+ * 22%), exactly as the SDK carries it on `tranche.apy` / `tranche.maxApy`.
78
+ * @param feePercent the chain's performance fee as a percentage, `0..100`
79
+ * (e.g. `10` for the live 10% fee).
80
+ * @returns the net compounded annual rate as a 0..1 fraction, or `NaN` when
81
+ * either input is outside its domain. Callers render `NaN` as "—".
82
+ */
83
+ export declare function netEffectiveApy(grossApy: number, feePercent: number): number;
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Epochs in a year — the single source of truth for every rate conversion in
3
+ * the SDK and in every consumer that undoes one.
4
+ *
5
+ * The subgraph carries a per-epoch `interestRate`; `epochRateToApy` compounds
6
+ * it into an APY and `apyToEpochRate` inverts exactly that. A second copy of
7
+ * this number anywhere would recover a different weekly rate and every derived
8
+ * figure — displayed APYs, net rates, projected interest — would be wrong, so
9
+ * `DataService.calculateApyForTranche` and `KSULocking.calculateApy` both read
10
+ * it from here rather than declaring their own.
11
+ */
12
+ export const EPOCHS_IN_YEAR = 52.17857;
13
+ /**
14
+ * A per-epoch interest rate compounded into an annual rate.
15
+ *
16
+ * ```
17
+ * apy = (1 + r)^E − 1
18
+ * ```
19
+ *
20
+ * This is the exact expression `DataService.calculateApyForTranche` has always
21
+ * used, kept in its literal `**` form rather than rewritten through
22
+ * `expm1`/`log1p`: it is the SDK's OWN definition of `tranche.apy`, so its
23
+ * float behaviour is part of the contract every consumer already calibrated
24
+ * against. `apyToEpochRate` is the one that must be numerically careful,
25
+ * because it runs on the result.
26
+ *
27
+ * @param epochRate the per-epoch rate as a 0..1 fraction (e.g. `0.003`).
28
+ * @returns the compounded annual rate as a 0..1 fraction.
29
+ */
30
+ export function epochRateToApy(epochRate) {
31
+ return Math.pow((1 + epochRate), EPOCHS_IN_YEAR) - 1;
32
+ }
33
+ /**
34
+ * The inverse of `epochRateToApy` — the per-epoch rate an APY was compounded
35
+ * from.
36
+ *
37
+ * `expm1`/`log1p` rather than the literal `(1 + apy) ** (1 / E) - 1`.
38
+ * Algebraically identical, but the direct form computes a double just above 1
39
+ * and subtracts 1 from it, discarding the low bits of a result that is itself
40
+ * ~1e-3 for a realistic rate. These two never form the intermediate near-1
41
+ * value.
42
+ *
43
+ * @param apy the compounded annual rate as a 0..1 fraction.
44
+ * @returns the per-epoch rate as a 0..1 fraction.
45
+ */
46
+ export function apyToEpochRate(apy) {
47
+ return Math.expm1(Math.log1p(apy) / EPOCHS_IN_YEAR);
48
+ }
49
+ /**
50
+ * The NET Effective Interest Rate — the compounded annual rate a lender
51
+ * actually earns, after the platform performance fee.
52
+ *
53
+ * ```
54
+ * r = (1 + grossApy)^(1/E) − 1 // the gross per-epoch rate
55
+ * net = (1 + r · (1 − feePercent/100))^E − 1 // fee taken each epoch, then compounded
56
+ * ```
57
+ *
58
+ * `LendingPool._applyTrancheInterest` mints the lender
59
+ * `interest × (1 − performanceFee)` at every epoch close, and it is that NET
60
+ * amount which capitalises and earns interest in the following epoch. So the
61
+ * fee is applied per epoch and the result is re-compounded — not deducted from
62
+ * the annual figure.
63
+ *
64
+ * ⚠️ UNITS — the one thing that must not be got wrong.
65
+ * `feePercent` is a PERCENTAGE IN 0..100, **not** a 0..1 fraction.
66
+ * `DataService.getPerformanceFee()` (and `StrategiesFacade.getPerformanceFeePercent()`)
67
+ * returns the subgraph's `integerToPercentage2(1000)` = `10`, meaning **ten
68
+ * percent**. It is NOT `0.10`. Treating it as a fraction computes
69
+ * `r · (1 − 10)` = `−9r` and yields a nonsense negative rate that would render
70
+ * as a plausible-looking `-…%`, which is why `feePercent > 100` and
71
+ * `feePercent < 0` both return `NaN` and why the parameter is named
72
+ * `feePercent` and never `fee`.
73
+ *
74
+ * This returns a NUMBER and nothing else. Rendering it — the 2dp convention,
75
+ * the ` p.a.` suffix, and the fail-closed em-dash for a `NaN` or non-positive
76
+ * result — is the consumer's job: kasu-ui does it in
77
+ * `src/features/lending/lib/interest-rate.ts` (`formatEffectiveRate`,
78
+ * `RATE_UNAVAILABLE`), which now wraps this function instead of restating it.
79
+ *
80
+ * @param grossApy compounded GROSS APY as a 0..1 fraction (e.g. `0.22` for
81
+ * 22%), exactly as the SDK carries it on `tranche.apy` / `tranche.maxApy`.
82
+ * @param feePercent the chain's performance fee as a percentage, `0..100`
83
+ * (e.g. `10` for the live 10% fee).
84
+ * @returns the net compounded annual rate as a 0..1 fraction, or `NaN` when
85
+ * either input is outside its domain. Callers render `NaN` as "—".
86
+ */
87
+ export function netEffectiveApy(grossApy, feePercent) {
88
+ if (!Number.isFinite(grossApy) || grossApy < 0)
89
+ return NaN;
90
+ if (!Number.isFinite(feePercent) || feePercent < 0 || feePercent > 100) {
91
+ return NaN;
92
+ }
93
+ // The lender's share of each epoch's interest. Short-circuiting on `=== 1`
94
+ // (rather than on `feePercent === 0`) also covers a fee small enough to
95
+ // underflow the multiplier: if it cannot change the epoch rate, the net
96
+ // rate IS the gross rate, and it must be returned unchanged rather than
97
+ // round-tripped through the de-compound/re-compound pair below — which is
98
+ // exact in algebra but not in floating point (0.22 comes back as
99
+ // 0.22000000000000663 in the naive form). If the fee is ever set to zero
100
+ // on-chain, the displayed rate must equal the SDK's APY exactly.
101
+ const lenderShare = 1 - feePercent / 100;
102
+ if (lenderShare === 1)
103
+ return grossApy;
104
+ // `expm1`/`log1p` rather than the literal `(1 + x) ** n - 1`. Algebraically
105
+ // identical, but the direct form computes `(1 + grossApy) ** (1 / E) - 1`
106
+ // by subtracting 1 from a double just above 1, discarding the low bits of a
107
+ // result that is itself ~1e-8 for a realistic rate. The ~1e-8 relative
108
+ // error that survives is enough to push the net rate ABOVE the gross one;
109
+ // these two never form the intermediate near-1 value.
110
+ const grossEpochRate = apyToEpochRate(grossApy);
111
+ const netEpochRate = grossEpochRate * lenderShare;
112
+ const net = Math.expm1(EPOCHS_IN_YEAR * Math.log1p(netEpochRate));
113
+ // `net ≤ grossApy` for every `lenderShare ≤ 1` is a theorem, but at the
114
+ // last ULP it is not a float identity — a fee of 1e-14 percent leaves a
115
+ // `lenderShare` one ULP below 1 and the round trip can land one ULP above
116
+ // `grossApy`. Clamping enforces the proven bound rather than letting a
117
+ // rounding artefact put a net rate above the gross rate it came from.
118
+ return Math.min(net, grossApy);
119
+ }
120
+ //# sourceMappingURL=rates.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rates.js","sourceRoot":"","sources":["../../src/domain/rates.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,QAAQ,CAAC;AAEvC;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,cAAc,CAAC,SAAiB;IAC5C,OAAO,SAAA,CAAC,CAAC,GAAG,SAAS,CAAC,EAAI,cAAc,CAAA,GAAG,CAAC,CAAC;AACjD,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,cAAc,CAAC,GAAW;IACtC,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,cAAc,CAAC,CAAC;AACxD,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,MAAM,UAAU,eAAe,CAAC,QAAgB,EAAE,UAAkB;IAChE,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,QAAQ,GAAG,CAAC;QAAE,OAAO,GAAG,CAAC;IAC3D,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,UAAU,GAAG,CAAC,IAAI,UAAU,GAAG,GAAG,EAAE,CAAC;QACrE,OAAO,GAAG,CAAC;IACf,CAAC;IACD,2EAA2E;IAC3E,wEAAwE;IACxE,wEAAwE;IACxE,wEAAwE;IACxE,0EAA0E;IAC1E,iEAAiE;IACjE,yEAAyE;IACzE,iEAAiE;IACjE,MAAM,WAAW,GAAG,CAAC,GAAG,UAAU,GAAG,GAAG,CAAC;IACzC,IAAI,WAAW,KAAK,CAAC;QAAE,OAAO,QAAQ,CAAC;IACvC,4EAA4E;IAC5E,0EAA0E;IAC1E,4EAA4E;IAC5E,uEAAuE;IACvE,0EAA0E;IAC1E,sDAAsD;IACtD,MAAM,cAAc,GAAG,cAAc,CAAC,QAAQ,CAAC,CAAC;IAChD,MAAM,YAAY,GAAG,cAAc,GAAG,WAAW,CAAC;IAClD,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,cAAc,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC,CAAC;IAClE,wEAAwE;IACxE,wEAAwE;IACxE,0EAA0E;IAC1E,uEAAuE;IACvE,sEAAsE;IACtE,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;AACnC,CAAC"}
@@ -0,0 +1 @@
1
+ export {};