@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,62 @@
1
+ import { TrancheData } from '../services/DataService/types';
2
+ export declare const MIN_LENDING_AMOUNT_FALLBACK = 500;
3
+ export declare const MAX_LENDING_AMOUNT_FALLBACK = 500000;
4
+ export declare function parseTrancheBound(raw: string | number | null | undefined): number | null;
5
+ export interface DepositBounds {
6
+ minDeposit: number;
7
+ maxDeposit: number;
8
+ }
9
+ /**
10
+ * Min + max lending amount (in whole stable units) for a tranche. The max is
11
+ * clamped by the tranche's *remaining* capacity: a tranche can fill mid-epoch
12
+ * while `maximumDeposit` stays at its configured value, so without the clamp
13
+ * a form would accept an amount the contract will reject. Mirrors
14
+ * kasu-fe-next's `calculateDepositMinMax` (`trancheMax = min(max, capacity)`).
15
+ */
16
+ export declare function resolveDepositBounds(tranche: TrancheData | undefined): DepositBounds;
17
+ export interface BoundShortcuts {
18
+ min: number;
19
+ max: number;
20
+ enabled: boolean;
21
+ }
22
+ /**
23
+ * The values a lend form's clickable Min/Max labels write into the amount
24
+ * field. Derived from the validated bounds but snapped to whole cents
25
+ * CONSERVATIVELY — the min rounds UP, the max rounds DOWN — so a shortcut can
26
+ * never fill an amount the form's own `min ≤ amount ≤ max` check rejects, and
27
+ * never carries the SDK's float noise into the field (`targetDrawAmount -
28
+ * pendingDeposits` routinely yields `249999.99999999997`, which would make the
29
+ * label, the field's 2dp display and the signed request label all disagree).
30
+ *
31
+ * `enabled` is false when the snapped range is empty: a tranche at capacity
32
+ * (max clamps to 0 or to sub-cent dust), a remaining capacity below the
33
+ * tranche minimum, or a jurisdiction floor above the capacity. Nothing either
34
+ * shortcut could write would validate, so both render inert.
35
+ */
36
+ export declare function resolveBoundShortcuts(minDeposit: number, maxDeposit: number): BoundShortcuts;
37
+ /**
38
+ * True when no amount exists that satisfies both bounds — the minimum sits
39
+ * ABOVE the maximum, so the range a form would otherwise print ("Min 360,000 ·
40
+ * Max 25,285") is a contradiction, not an instruction. Happens when a
41
+ * tranche's remaining capacity falls under its own configured minimum, when it
42
+ * is at capacity (max 0 / sub-cent dust), or when the AU wholesale cumulative
43
+ * floor lands above the capacity that is left.
44
+ *
45
+ * Defined as the negation of `resolveBoundShortcuts().enabled` on purpose: the
46
+ * "there is nothing to click" and "there is nothing to type" states must never
47
+ * be able to disagree.
48
+ *
49
+ * This is UX only. The binding minimum is enforced by kasu-backend (403
50
+ * `AU_WHOLESALE_MINIMUM_NOT_MET`) and by the contract's own bounds; nothing
51
+ * here may be used to relax either.
52
+ */
53
+ export declare function isBelowMinimumCapacity(minDeposit: number, maxDeposit: number): boolean;
54
+ /**
55
+ * Largest whole-cent value ≤ `n`. Float multiplication can round `n * 100` UP
56
+ * onto an integer (`1.15 * 100 === 114.99999999999999` is the usual direction,
57
+ * but the other happens too), so the result is re-checked against `n` and
58
+ * stepped down a cent if it overshot — the contract is "never above `n`".
59
+ */
60
+ export declare function floorToCents(n: number): number;
61
+ /** Smallest whole-cent value ≥ `n`. Mirror of `floorToCents`: never below `n`. */
62
+ export declare function ceilToCents(n: number): number;
@@ -0,0 +1,84 @@
1
+ // Fallbacks if the selected tranche's on-chain min/max are 0 / NaN (some
2
+ // "coming soon" pools haven't been configured yet).
3
+ export const MIN_LENDING_AMOUNT_FALLBACK = 500;
4
+ export const MAX_LENDING_AMOUNT_FALLBACK = 500000;
5
+ export function parseTrancheBound(raw) {
6
+ if (raw == null)
7
+ return null;
8
+ const n = typeof raw === 'string' ? parseFloat(raw) : raw;
9
+ return Number.isFinite(n) && n > 0 ? n : null;
10
+ }
11
+ /**
12
+ * Min + max lending amount (in whole stable units) for a tranche. The max is
13
+ * clamped by the tranche's *remaining* capacity: a tranche can fill mid-epoch
14
+ * while `maximumDeposit` stays at its configured value, so without the clamp
15
+ * a form would accept an amount the contract will reject. Mirrors
16
+ * kasu-fe-next's `calculateDepositMinMax` (`trancheMax = min(max, capacity)`).
17
+ */
18
+ export function resolveDepositBounds(tranche) {
19
+ var _a, _b;
20
+ const minDeposit = (_a = parseTrancheBound(tranche === null || tranche === void 0 ? void 0 : tranche.minimumDeposit)) !== null && _a !== void 0 ? _a : MIN_LENDING_AMOUNT_FALLBACK;
21
+ const configuredMax = (_b = parseTrancheBound(tranche === null || tranche === void 0 ? void 0 : tranche.maximumDeposit)) !== null && _b !== void 0 ? _b : MAX_LENDING_AMOUNT_FALLBACK;
22
+ if (!tranche)
23
+ return { minDeposit, maxDeposit: configuredMax };
24
+ const remainingCapacity = Math.max(0, parseFloat(tranche.poolCapacity) || 0);
25
+ return { minDeposit, maxDeposit: Math.min(configuredMax, remainingCapacity) };
26
+ }
27
+ /**
28
+ * The values a lend form's clickable Min/Max labels write into the amount
29
+ * field. Derived from the validated bounds but snapped to whole cents
30
+ * CONSERVATIVELY — the min rounds UP, the max rounds DOWN — so a shortcut can
31
+ * never fill an amount the form's own `min ≤ amount ≤ max` check rejects, and
32
+ * never carries the SDK's float noise into the field (`targetDrawAmount -
33
+ * pendingDeposits` routinely yields `249999.99999999997`, which would make the
34
+ * label, the field's 2dp display and the signed request label all disagree).
35
+ *
36
+ * `enabled` is false when the snapped range is empty: a tranche at capacity
37
+ * (max clamps to 0 or to sub-cent dust), a remaining capacity below the
38
+ * tranche minimum, or a jurisdiction floor above the capacity. Nothing either
39
+ * shortcut could write would validate, so both render inert.
40
+ */
41
+ export function resolveBoundShortcuts(minDeposit, maxDeposit) {
42
+ const min = ceilToCents(minDeposit);
43
+ const max = floorToCents(maxDeposit);
44
+ const enabled = Number.isFinite(min) && Number.isFinite(max) && max > 0 && max >= min;
45
+ return { min, max, enabled };
46
+ }
47
+ /**
48
+ * True when no amount exists that satisfies both bounds — the minimum sits
49
+ * ABOVE the maximum, so the range a form would otherwise print ("Min 360,000 ·
50
+ * Max 25,285") is a contradiction, not an instruction. Happens when a
51
+ * tranche's remaining capacity falls under its own configured minimum, when it
52
+ * is at capacity (max 0 / sub-cent dust), or when the AU wholesale cumulative
53
+ * floor lands above the capacity that is left.
54
+ *
55
+ * Defined as the negation of `resolveBoundShortcuts().enabled` on purpose: the
56
+ * "there is nothing to click" and "there is nothing to type" states must never
57
+ * be able to disagree.
58
+ *
59
+ * This is UX only. The binding minimum is enforced by kasu-backend (403
60
+ * `AU_WHOLESALE_MINIMUM_NOT_MET`) and by the contract's own bounds; nothing
61
+ * here may be used to relax either.
62
+ */
63
+ export function isBelowMinimumCapacity(minDeposit, maxDeposit) {
64
+ return !resolveBoundShortcuts(minDeposit, maxDeposit).enabled;
65
+ }
66
+ const CENTS = 100;
67
+ /**
68
+ * Largest whole-cent value ≤ `n`. Float multiplication can round `n * 100` UP
69
+ * onto an integer (`1.15 * 100 === 114.99999999999999` is the usual direction,
70
+ * but the other happens too), so the result is re-checked against `n` and
71
+ * stepped down a cent if it overshot — the contract is "never above `n`".
72
+ */
73
+ export function floorToCents(n) {
74
+ const k = Math.floor(n * CENTS);
75
+ const snapped = k / CENTS;
76
+ return snapped > n ? (k - 1) / CENTS : snapped;
77
+ }
78
+ /** Smallest whole-cent value ≥ `n`. Mirror of `floorToCents`: never below `n`. */
79
+ export function ceilToCents(n) {
80
+ const k = Math.ceil(n * CENTS);
81
+ const snapped = k / CENTS;
82
+ return snapped < n ? (k + 1) / CENTS : snapped;
83
+ }
84
+ //# sourceMappingURL=deposit-bounds.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deposit-bounds.js","sourceRoot":"","sources":["../../src/domain/deposit-bounds.ts"],"names":[],"mappings":"AAEA,yEAAyE;AACzE,oDAAoD;AACpD,MAAM,CAAC,MAAM,2BAA2B,GAAG,GAAG,CAAC;AAC/C,MAAM,CAAC,MAAM,2BAA2B,GAAG,MAAO,CAAC;AAEnD,MAAM,UAAU,iBAAiB,CAC7B,GAAuC;IAEvC,IAAI,GAAG,IAAI,IAAI;QAAE,OAAO,IAAI,CAAC;IAC7B,MAAM,CAAC,GAAG,OAAO,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;IAC1D,OAAO,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AAClD,CAAC;AAOD;;;;;;GAMG;AACH,MAAM,UAAU,oBAAoB,CAChC,OAAgC;;IAEhC,MAAM,UAAU,GACZ,MAAA,iBAAiB,CAAC,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAE,cAAc,CAAC,mCAC1C,2BAA2B,CAAC;IAChC,MAAM,aAAa,GACf,MAAA,iBAAiB,CAAC,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAE,cAAc,CAAC,mCAC1C,2BAA2B,CAAC;IAChC,IAAI,CAAC,OAAO;QAAE,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,aAAa,EAAE,CAAC;IAC/D,MAAM,iBAAiB,GAAG,IAAI,CAAC,GAAG,CAC9B,CAAC,EACD,UAAU,CAAC,OAAO,CAAC,YAAY,CAAC,IAAI,CAAC,CACxC,CAAC;IACF,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,IAAI,CAAC,GAAG,CAAC,aAAa,EAAE,iBAAiB,CAAC,EAAE,CAAC;AAClF,CAAC;AAQD;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,qBAAqB,CACjC,UAAkB,EAClB,UAAkB;IAElB,MAAM,GAAG,GAAG,WAAW,CAAC,UAAU,CAAC,CAAC;IACpC,MAAM,GAAG,GAAG,YAAY,CAAC,UAAU,CAAC,CAAC;IACrC,MAAM,OAAO,GACT,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,GAAG,GAAG,CAAC,IAAI,GAAG,IAAI,GAAG,CAAC;IAC1E,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC;AACjC,CAAC;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,sBAAsB,CAClC,UAAkB,EAClB,UAAkB;IAElB,OAAO,CAAC,qBAAqB,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC,OAAO,CAAC;AAClE,CAAC;AAED,MAAM,KAAK,GAAG,GAAG,CAAC;AAElB;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAAC,CAAS;IAClC,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC;IAChC,MAAM,OAAO,GAAG,CAAC,GAAG,KAAK,CAAC;IAC1B,OAAO,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC;AACnD,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,WAAW,CAAC,CAAS;IACjC,MAAM,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC;IAC/B,MAAM,OAAO,GAAG,CAAC,GAAG,KAAK,CAAC;IAC1B,OAAO,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC;AACnD,CAAC"}
@@ -0,0 +1,135 @@
1
+ import * as fc from 'fast-check';
2
+ import { ceilToCents, floorToCents, isBelowMinimumCapacity, MAX_LENDING_AMOUNT_FALLBACK, MIN_LENDING_AMOUNT_FALLBACK, parseTrancheBound, resolveBoundShortcuts, resolveDepositBounds, } from './deposit-bounds';
3
+ /**
4
+ * Ported from kasu-ui `src/features/lending/lib/tranche-bounds.property.test.ts`,
5
+ * with the `resolveDepositBounds` / `parseTrancheBound` cases the property file
6
+ * did not cover.
7
+ *
8
+ * The Min/Max shortcuts (clickable bound labels on a lend form) must never
9
+ * write an amount the form rejects, so the snapping is checked as invariants
10
+ * over the whole input domain rather than at a few hand-picked values.
11
+ */
12
+ function tranche(over) {
13
+ return Object.assign({ minimumDeposit: '500', maximumDeposit: '500000', poolCapacity: '250000' }, over);
14
+ }
15
+ const amountArb = fc.double({
16
+ min: 0,
17
+ max: 1e12,
18
+ noNaN: true,
19
+ noDefaultInfinity: true,
20
+ });
21
+ /** `String(k / 100)` prints at most two decimals for any integer `k` in range. */
22
+ const AT_MOST_TWO_DP = /^\d+(\.\d{1,2})?$/;
23
+ describe('parseTrancheBound', () => {
24
+ it('accepts a positive finite bound from a string or a number', () => {
25
+ expect(parseTrancheBound('500')).toBe(500);
26
+ expect(parseTrancheBound(500)).toBe(500);
27
+ });
28
+ it('rejects anything that is not a usable bound', () => {
29
+ expect(parseTrancheBound(null)).toBeNull();
30
+ expect(parseTrancheBound(undefined)).toBeNull();
31
+ expect(parseTrancheBound('')).toBeNull();
32
+ expect(parseTrancheBound('0')).toBeNull();
33
+ expect(parseTrancheBound('-1')).toBeNull();
34
+ expect(parseTrancheBound('not-a-number')).toBeNull();
35
+ expect(parseTrancheBound(Infinity)).toBeNull();
36
+ });
37
+ });
38
+ describe('resolveDepositBounds', () => {
39
+ it('clamps the max by the tranche’s REMAINING capacity', () => {
40
+ // A tranche can fill mid-epoch while `maximumDeposit` stays put.
41
+ expect(resolveDepositBounds(tranche({ maximumDeposit: '500000', poolCapacity: '25285.86' }))).toEqual({ minDeposit: 500, maxDeposit: 25285.86 });
42
+ });
43
+ it('keeps the configured max when capacity is larger', () => {
44
+ expect(resolveDepositBounds(tranche({ maximumDeposit: '500000', poolCapacity: '900000' }))).toEqual({ minDeposit: 500, maxDeposit: 500000 });
45
+ });
46
+ it('falls back when a "coming soon" pool has no bounds configured', () => {
47
+ expect(resolveDepositBounds(tranche({
48
+ minimumDeposit: '0',
49
+ maximumDeposit: '0',
50
+ poolCapacity: '900000',
51
+ }))).toEqual({
52
+ minDeposit: MIN_LENDING_AMOUNT_FALLBACK,
53
+ maxDeposit: MAX_LENDING_AMOUNT_FALLBACK,
54
+ });
55
+ });
56
+ it('uses both fallbacks and skips the capacity clamp with no tranche at all', () => {
57
+ expect(resolveDepositBounds(undefined)).toEqual({
58
+ minDeposit: MIN_LENDING_AMOUNT_FALLBACK,
59
+ maxDeposit: MAX_LENDING_AMOUNT_FALLBACK,
60
+ });
61
+ });
62
+ it('treats unparseable or negative capacity as zero', () => {
63
+ expect(resolveDepositBounds(tranche({ poolCapacity: 'not-a-number' }))
64
+ .maxDeposit).toBe(0);
65
+ expect(resolveDepositBounds(tranche({ poolCapacity: '-5' })).maxDeposit).toBe(0);
66
+ });
67
+ });
68
+ describe('cent snapping', () => {
69
+ it('floorToCents never exceeds its input, stays within a cent of it, and has ≤ 2dp', () => {
70
+ fc.assert(fc.property(amountArb, (n) => {
71
+ const snapped = floorToCents(n);
72
+ expect(snapped).toBeLessThanOrEqual(n);
73
+ expect(n - snapped).toBeLessThan(0.02);
74
+ expect(String(snapped)).toMatch(AT_MOST_TWO_DP);
75
+ }));
76
+ });
77
+ it('ceilToCents never undershoots its input, stays within a cent of it, and has ≤ 2dp', () => {
78
+ fc.assert(fc.property(amountArb, (n) => {
79
+ const snapped = ceilToCents(n);
80
+ expect(snapped).toBeGreaterThanOrEqual(n);
81
+ expect(snapped - n).toBeLessThan(0.02);
82
+ expect(String(snapped)).toMatch(AT_MOST_TWO_DP);
83
+ }));
84
+ });
85
+ });
86
+ describe('resolveBoundShortcuts', () => {
87
+ it('keeps both shortcuts inside the validated range whenever they are enabled', () => {
88
+ fc.assert(fc.property(amountArb, amountArb, (minDeposit, maxDeposit) => {
89
+ const { min, max, enabled } = resolveBoundShortcuts(minDeposit, maxDeposit);
90
+ if (!enabled)
91
+ return;
92
+ expect(min).toBeGreaterThanOrEqual(minDeposit);
93
+ expect(max).toBeLessThanOrEqual(maxDeposit);
94
+ expect(max).toBeGreaterThanOrEqual(min);
95
+ expect(max).toBeGreaterThan(0);
96
+ }));
97
+ });
98
+ it('disables the pair whenever the snapped range is empty', () => {
99
+ // Tranche at capacity (max clamps to 0), sub-cent dust the SDK's float
100
+ // subtraction leaves behind, remaining capacity below the tranche
101
+ // minimum, and an AU floor above the capacity.
102
+ expect(resolveBoundShortcuts(500, 0).enabled).toBe(false);
103
+ expect(resolveBoundShortcuts(500, 1.4551915228366852e-11).enabled).toBe(false);
104
+ expect(resolveBoundShortcuts(500, 137).enabled).toBe(false);
105
+ expect(resolveBoundShortcuts(240000, 100000).enabled).toBe(false);
106
+ });
107
+ it('snaps SDK float noise onto cents without crossing the bound', () => {
108
+ expect(resolveBoundShortcuts(500, 249999.99999999997)).toEqual({
109
+ min: 500,
110
+ max: 249999.99,
111
+ enabled: true,
112
+ });
113
+ expect(resolveBoundShortcuts(500, 25285.86)).toEqual({
114
+ min: 500,
115
+ max: 25285.86,
116
+ enabled: true,
117
+ });
118
+ // An AU remainder with more precision than the field shows rounds UP.
119
+ expect(resolveBoundShortcuts(358765.432109, 500000).min).toBe(358765.44);
120
+ });
121
+ });
122
+ describe('isBelowMinimumCapacity', () => {
123
+ it('is exactly the negation of the shortcuts being enabled', () => {
124
+ fc.assert(fc.property(amountArb, amountArb, (minDeposit, maxDeposit) => {
125
+ // The "nothing to click" and "nothing to type" states must
126
+ // never be able to disagree.
127
+ expect(isBelowMinimumCapacity(minDeposit, maxDeposit)).toBe(!resolveBoundShortcuts(minDeposit, maxDeposit).enabled);
128
+ }));
129
+ });
130
+ it('flags a capacity that has fallen under the tranche minimum', () => {
131
+ expect(isBelowMinimumCapacity(500, 137)).toBe(true);
132
+ expect(isBelowMinimumCapacity(500, 25285.86)).toBe(false);
133
+ });
134
+ });
135
+ //# sourceMappingURL=deposit-bounds.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deposit-bounds.test.js","sourceRoot":"","sources":["../../src/domain/deposit-bounds.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The shared domain layer: the rules every Kasu frontend needs to agree on,
3
+ * in one place instead of three.
4
+ *
5
+ * Two rules govern what may live here.
6
+ *
7
+ * 1. **Numbers and codes only.** No copy, no locale, no `Intl`, no string a
8
+ * user reads — `netEffectiveApy` returns `0.196`, never `'19.60% p.a.'`,
9
+ * and `derivePoolStatus` returns the code `'Full'`, not a sentence. The
10
+ * formatters that turn these into text stay in each app, where the design
11
+ * system and the visitor's locale are. The one exception is
12
+ * `getTrancheDisplayName`, which exists precisely because that rename must
13
+ * NOT drift between apps.
14
+ * 2. **Pure.** No network, no clock, no environment. Anything with I/O belongs
15
+ * in `facade/` (see `fetchUnusedPoolIds`).
16
+ */
17
+ export { apyToEpochRate, epochRateToApy, EPOCHS_IN_YEAR, netEffectiveApy, } from './rates';
18
+ export { ceilToCents, floorToCents, isBelowMinimumCapacity, MAX_LENDING_AMOUNT_FALLBACK, MIN_LENDING_AMOUNT_FALLBACK, parseTrancheBound, resolveBoundShortcuts, resolveDepositBounds, } from './deposit-bounds';
19
+ export type { BoundShortcuts, DepositBounds } from './deposit-bounds';
20
+ export { APXIUM, getCreditOriginator, getInstitutionalLender, INVOICEMATE, RIXON_CAPITAL, } from './partners';
21
+ export type { PoolNameSignal, StrategyPartner } from './partners';
22
+ export { maxNetRateCeiling, pickHighestYieldTranche, poolMaxApy, selectVisiblePools, } from './pools';
23
+ export type { BestTranche } from './pools';
24
+ export { getTrancheDisplayName, UPPER_MEZZANINE } from './tranche-display-name';
25
+ export { compareTrancheSeniority, derivePoolStatus, MIN_TRANCHE_CAPACITY, netTrancheApyBounds, pickDefaultTrancheId, poolAllTranchesFull, trancheApyBounds, trancheHasCapacity, trancheRiskRank, } from './tranches';
26
+ export type { ApyBounds, PoolStatus, TrancheCapacitySignal, } from './tranches';
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The shared domain layer: the rules every Kasu frontend needs to agree on,
3
+ * in one place instead of three.
4
+ *
5
+ * Two rules govern what may live here.
6
+ *
7
+ * 1. **Numbers and codes only.** No copy, no locale, no `Intl`, no string a
8
+ * user reads — `netEffectiveApy` returns `0.196`, never `'19.60% p.a.'`,
9
+ * and `derivePoolStatus` returns the code `'Full'`, not a sentence. The
10
+ * formatters that turn these into text stay in each app, where the design
11
+ * system and the visitor's locale are. The one exception is
12
+ * `getTrancheDisplayName`, which exists precisely because that rename must
13
+ * NOT drift between apps.
14
+ * 2. **Pure.** No network, no clock, no environment. Anything with I/O belongs
15
+ * in `facade/` (see `fetchUnusedPoolIds`).
16
+ */
17
+ export { apyToEpochRate, epochRateToApy, EPOCHS_IN_YEAR, netEffectiveApy, } from './rates';
18
+ export { ceilToCents, floorToCents, isBelowMinimumCapacity, MAX_LENDING_AMOUNT_FALLBACK, MIN_LENDING_AMOUNT_FALLBACK, parseTrancheBound, resolveBoundShortcuts, resolveDepositBounds, } from './deposit-bounds';
19
+ export { APXIUM, getCreditOriginator, getInstitutionalLender, INVOICEMATE, RIXON_CAPITAL, } from './partners';
20
+ export { maxNetRateCeiling, pickHighestYieldTranche, poolMaxApy, selectVisiblePools, } from './pools';
21
+ export { getTrancheDisplayName, UPPER_MEZZANINE } from './tranche-display-name';
22
+ export { compareTrancheSeniority, derivePoolStatus, MIN_TRANCHE_CAPACITY, netTrancheApyBounds, pickDefaultTrancheId, poolAllTranchesFull, trancheApyBounds, trancheHasCapacity, trancheRiskRank, } from './tranches';
23
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/domain/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EACH,cAAc,EACd,cAAc,EACd,cAAc,EACd,eAAe,GAClB,MAAM,SAAS,CAAC;AAEjB,OAAO,EACH,WAAW,EACX,YAAY,EACZ,sBAAsB,EACtB,2BAA2B,EAC3B,2BAA2B,EAC3B,iBAAiB,EACjB,qBAAqB,EACrB,oBAAoB,GACvB,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EACH,MAAM,EACN,mBAAmB,EACnB,sBAAsB,EACtB,WAAW,EACX,aAAa,GAChB,MAAM,YAAY,CAAC;AAGpB,OAAO,EACH,iBAAiB,EACjB,uBAAuB,EACvB,UAAU,EACV,kBAAkB,GACrB,MAAM,SAAS,CAAC;AAGjB,OAAO,EAAE,qBAAqB,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAEhF,OAAO,EACH,uBAAuB,EACvB,gBAAgB,EAChB,oBAAoB,EACpB,mBAAmB,EACnB,oBAAoB,EACpB,mBAAmB,EACnB,gBAAgB,EAChB,kBAAkB,EAClB,eAAe,GAClB,MAAM,YAAY,CAAC"}
@@ -0,0 +1,34 @@
1
+ export interface StrategyPartner {
2
+ name: string;
3
+ url: string;
4
+ }
5
+ export declare const APXIUM: StrategyPartner;
6
+ export declare const INVOICEMATE: StrategyPartner;
7
+ export declare const RIXON_CAPITAL: StrategyPartner;
8
+ /**
9
+ * Anything a pool name can arrive as. The raw `PoolOverview` carries
10
+ * `poolName`, the facade's `Strategy` carries `name`, and a caller who already
11
+ * has the string can pass it directly — one rule, whichever shape reaches it.
12
+ * `poolName` wins when an object carries both and it is set.
13
+ */
14
+ export type PoolNameSignal = string | {
15
+ poolName?: string | null;
16
+ } | {
17
+ name?: string | null;
18
+ };
19
+ /**
20
+ * Credit-originator inference by pool NAME only. The sole InvoiceMate
21
+ * strategy is "Payment Finance (PayFi)" (matched by "payfi" / "payment
22
+ * finance"); every other pool — Whole Ledger / Taxation / Professional Fee
23
+ * Funding — is Apxium.
24
+ *
25
+ * We deliberately do NOT match on asset class / subheading: Apxium's pools
26
+ * are receivables/invoice financing, so their asset class contains
27
+ * "invoice" and would be misclassified as InvoiceMate.
28
+ */
29
+ export declare function getCreditOriginator(pool: PoolNameSignal): StrategyPartner;
30
+ /**
31
+ * Apxium pools carry an institutional senior lender (Rixon Capital);
32
+ * InvoiceMate pools have none.
33
+ */
34
+ export declare function getInstitutionalLender(originator: StrategyPartner): StrategyPartner | null;
@@ -0,0 +1,43 @@
1
+ export const APXIUM = {
2
+ name: 'Apxium',
3
+ url: 'https://www.apxium.com/',
4
+ };
5
+ export const INVOICEMATE = {
6
+ name: 'InvoiceMate',
7
+ url: 'https://invoicemate.net/',
8
+ };
9
+ export const RIXON_CAPITAL = {
10
+ name: 'Rixon Capital',
11
+ url: 'https://rixon.capital/',
12
+ };
13
+ function poolNameOf(pool) {
14
+ var _a, _b;
15
+ if (typeof pool === 'string')
16
+ return pool;
17
+ const signal = pool;
18
+ return (_b = (_a = signal.poolName) !== null && _a !== void 0 ? _a : signal.name) !== null && _b !== void 0 ? _b : '';
19
+ }
20
+ /**
21
+ * Credit-originator inference by pool NAME only. The sole InvoiceMate
22
+ * strategy is "Payment Finance (PayFi)" (matched by "payfi" / "payment
23
+ * finance"); every other pool — Whole Ledger / Taxation / Professional Fee
24
+ * Funding — is Apxium.
25
+ *
26
+ * We deliberately do NOT match on asset class / subheading: Apxium's pools
27
+ * are receivables/invoice financing, so their asset class contains
28
+ * "invoice" and would be misclassified as InvoiceMate.
29
+ */
30
+ export function getCreditOriginator(pool) {
31
+ const name = poolNameOf(pool).toLowerCase();
32
+ if (/payfi|payment finance/.test(name))
33
+ return INVOICEMATE;
34
+ return APXIUM;
35
+ }
36
+ /**
37
+ * Apxium pools carry an institutional senior lender (Rixon Capital);
38
+ * InvoiceMate pools have none.
39
+ */
40
+ export function getInstitutionalLender(originator) {
41
+ return originator.name === APXIUM.name ? RIXON_CAPITAL : null;
42
+ }
43
+ //# sourceMappingURL=partners.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"partners.js","sourceRoot":"","sources":["../../src/domain/partners.ts"],"names":[],"mappings":"AAWA,MAAM,CAAC,MAAM,MAAM,GAAoB;IACnC,IAAI,EAAE,QAAQ;IACd,GAAG,EAAE,yBAAyB;CACjC,CAAC;AACF,MAAM,CAAC,MAAM,WAAW,GAAoB;IACxC,IAAI,EAAE,aAAa;IACnB,GAAG,EAAE,0BAA0B;CAClC,CAAC;AACF,MAAM,CAAC,MAAM,aAAa,GAAoB;IAC1C,IAAI,EAAE,eAAe;IACrB,GAAG,EAAE,wBAAwB;CAChC,CAAC;AAaF,SAAS,UAAU,CAAC,IAAoB;;IACpC,IAAI,OAAO,IAAI,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IAC1C,MAAM,MAAM,GAAuD,IAAI,CAAC;IACxE,OAAO,MAAA,MAAA,MAAM,CAAC,QAAQ,mCAAI,MAAM,CAAC,IAAI,mCAAI,EAAE,CAAC;AAChD,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,mBAAmB,CAAC,IAAoB;IACpD,MAAM,IAAI,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC;IAC5C,IAAI,uBAAuB,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,WAAW,CAAC;IAC3D,OAAO,MAAM,CAAC;AAClB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,sBAAsB,CAClC,UAA2B;IAE3B,OAAO,UAAU,CAAC,IAAI,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC;AAClE,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,53 @@
1
+ import { APXIUM, getCreditOriginator, getInstitutionalLender, INVOICEMATE, RIXON_CAPITAL, } from './partners';
2
+ // Ported from kasu-ui `src/features/lending/lib/strategy-partners.test.ts`.
3
+ describe('getCreditOriginator', () => {
4
+ it('maps "Payment Finance (PayFi)" to InvoiceMate', () => {
5
+ expect(getCreditOriginator({ poolName: 'Payment Finance (PayFi)' })).toBe(INVOICEMATE);
6
+ });
7
+ it('keeps Apxium receivables pools as Apxium (regression: do not match "invoice")', () => {
8
+ // The bug: matching the asset class ("invoice financing") wrongly
9
+ // classified Apxium receivables pools as InvoiceMate. Name-only
10
+ // matching fixes it — none of these names contain payfi/payment
11
+ // finance.
12
+ expect(getCreditOriginator({ poolName: 'Professional Fee Funding' })).toBe(APXIUM);
13
+ expect(getCreditOriginator({ poolName: 'Whole Ledger Funding' })).toBe(APXIUM);
14
+ expect(getCreditOriginator({ poolName: 'Taxation Funding (Tax Pay)' })).toBe(APXIUM);
15
+ });
16
+ it('is case-insensitive and matches the "payfi" / "payment finance" name', () => {
17
+ expect(getCreditOriginator({ poolName: 'payment finance' })).toBe(INVOICEMATE);
18
+ expect(getCreditOriginator({ poolName: 'PAYFI Strategy' })).toBe(INVOICEMATE);
19
+ });
20
+ it('defaults to Apxium for empty / missing names', () => {
21
+ expect(getCreditOriginator({ poolName: '' })).toBe(APXIUM);
22
+ expect(getCreditOriginator({})).toBe(APXIUM);
23
+ });
24
+ });
25
+ describe('getCreditOriginator — the widened PoolNameSignal', () => {
26
+ it('reads the facade Strategy shape (`name`)', () => {
27
+ expect(getCreditOriginator({ name: 'Payment Finance (PayFi)' })).toBe(INVOICEMATE);
28
+ expect(getCreditOriginator({ name: 'Taxation Funding' })).toBe(APXIUM);
29
+ expect(getCreditOriginator({ name: null })).toBe(APXIUM);
30
+ });
31
+ it('reads a bare string', () => {
32
+ expect(getCreditOriginator('Payment Finance (PayFi)')).toBe(INVOICEMATE);
33
+ expect(getCreditOriginator('Whole Ledger Funding')).toBe(APXIUM);
34
+ expect(getCreditOriginator('')).toBe(APXIUM);
35
+ });
36
+ it('prefers poolName when an object carries both', () => {
37
+ const both = { poolName: 'Taxation Funding', name: 'Payment Finance' };
38
+ expect(getCreditOriginator(both)).toBe(APXIUM);
39
+ });
40
+ it('treats an explicitly null poolName as missing, not as a match', () => {
41
+ expect(getCreditOriginator({ poolName: null })).toBe(APXIUM);
42
+ expect(getCreditOriginator({ poolName: undefined })).toBe(APXIUM);
43
+ });
44
+ });
45
+ describe('getInstitutionalLender', () => {
46
+ it('gives Apxium pools Rixon Capital', () => {
47
+ expect(getInstitutionalLender(APXIUM)).toBe(RIXON_CAPITAL);
48
+ });
49
+ it('gives InvoiceMate pools no senior lender', () => {
50
+ expect(getInstitutionalLender(INVOICEMATE)).toBeNull();
51
+ });
52
+ });
53
+ //# sourceMappingURL=partners.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"partners.test.js","sourceRoot":"","sources":["../../src/domain/partners.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,70 @@
1
+ import { PoolOverview, TrancheData } from '../services/DataService/types';
2
+ /**
3
+ * The highest FTD-inclusive upper bound across a pool's tranches, or `0` when
4
+ * none is usable. The sort key behind `selectVisiblePools`.
5
+ */
6
+ export declare function poolMaxApy(pool: PoolOverview): number;
7
+ /**
8
+ * A lending grid's visible pool set: *active* and *not yet oversubscribed*
9
+ * pools, sorted with has-capacity strategies first, then by highest max APY.
10
+ *
11
+ * Shared so a server-rendered payload and a client re-fetch can never
12
+ * disagree — kasu-ui's SSR fetcher and client hook used to carry verbatim
13
+ * copies of this filter + sort, and any drift surfaced as an SSR→CSR content
14
+ * flip on the lending page.
15
+ *
16
+ * Pure: operates on a fresh array (`filter` output), so the in-place `sort`
17
+ * does not mutate the caller's input.
18
+ */
19
+ export declare function selectVisiblePools(all: PoolOverview[]): PoolOverview[];
20
+ export interface BestTranche {
21
+ pool: PoolOverview;
22
+ tranche: TrancheData;
23
+ /** Base variable-rate APY as a decimal fraction (e.g. `0.12`). */
24
+ apy: number;
25
+ }
26
+ /**
27
+ * The highest-yielding tranche that still has capacity, across all pools.
28
+ *
29
+ * Compares `tranche.apy` — the base variable rate — NOT `minApy`/`maxApy`,
30
+ * which the data service widens with fixed-term-deposit variants. A headline
31
+ * naming one tranche should advertise the rate every depositor gets, not an
32
+ * FTD-locked uplift.
33
+ *
34
+ * Capacity is gated by the shared `trancheHasCapacity` (≥ 1 stable unit), so
35
+ * full tranches never win. Ties on APY break toward more remaining capacity.
36
+ * Returns `null` when no tranche qualifies (empty list, all full, or all rates
37
+ * unset) — callers fall back to static copy.
38
+ */
39
+ export declare function pickHighestYieldTranche(pools?: PoolOverview[] | null): BestTranche | null;
40
+ /**
41
+ * The highest NET Effective Interest Rate on offer across the pools passed in
42
+ * — the ceiling behind an "up to …" claim. The numeric core of kasu-ui's
43
+ * `formatMaxNetRate`.
44
+ *
45
+ * `null` when there is nothing to quote, and callers must then DROP the claim
46
+ * rather than print a placeholder: this feeds text that gets posted publicly.
47
+ *
48
+ * Scope follows the caller's pool list, which is normally keyed to one chain —
49
+ * so a lender on XDC AUDD quotes the AUDD ceiling, not Base's. That is the
50
+ * intended reading: they are sharing the deployment they are on.
51
+ *
52
+ * Reads `maxApy` — the FTD-inclusive upper bound — unlike
53
+ * `pickHighestYieldTranche` above, which names one tranche's variable rate. A
54
+ * ceiling claim and a specific tranche's rate are different statements, and
55
+ * the ceiling must agree with the top of `netTrancheApyBounds`, which reads
56
+ * the same field. `apy` is taken into the comparison too: `maxApy` is derived
57
+ * from the base rate plus any fixed-term configs, so it can never be lower,
58
+ * and a tranche that carries no `maxApy` at all still contributes its base
59
+ * rate instead of dropping out.
60
+ *
61
+ * Capacity-gated by `trancheHasCapacity` — we never advertise a rate no lender
62
+ * could take up.
63
+ *
64
+ * FAIL CLOSED: an out-of-domain fee, or a net rate that is non-finite or ≤ 0,
65
+ * returns `null`. The gross figure is never a substitute, and a caller that
66
+ * does not yet know the fee must not pass `0`.
67
+ *
68
+ * @param feePercent the chain's performance fee, 0..100 — never a fraction.
69
+ */
70
+ export declare function maxNetRateCeiling(pools: PoolOverview[] | undefined | null, feePercent: number): number | null;