@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.
Files changed (180) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +201 -253
  3. package/dist/bundle.cjs.js +5472 -1710
  4. package/dist/bundle.esm.js +5415 -1713
  5. package/dist/domain/au-minimum.d.ts +135 -0
  6. package/dist/domain/au-minimum.js +154 -0
  7. package/dist/domain/au-minimum.js.map +1 -0
  8. package/dist/domain/au-minimum.test.js +202 -0
  9. package/dist/domain/au-minimum.test.js.map +1 -0
  10. package/dist/domain/deposit-bounds.d.ts +62 -0
  11. package/dist/domain/deposit-bounds.js +84 -0
  12. package/dist/domain/deposit-bounds.js.map +1 -0
  13. package/dist/domain/deposit-bounds.test.d.ts +1 -0
  14. package/dist/domain/deposit-bounds.test.js +135 -0
  15. package/dist/domain/deposit-bounds.test.js.map +1 -0
  16. package/dist/domain/index.d.ts +40 -0
  17. package/dist/domain/index.js +33 -0
  18. package/dist/domain/index.js.map +1 -0
  19. package/dist/domain/loan-contract.d.ts +174 -0
  20. package/dist/domain/loan-contract.js +160 -0
  21. package/dist/domain/loan-contract.js.map +1 -0
  22. package/dist/domain/loan-contract.test.d.ts +1 -0
  23. package/dist/domain/loan-contract.test.js +255 -0
  24. package/dist/domain/loan-contract.test.js.map +1 -0
  25. package/dist/domain/partners.d.ts +34 -0
  26. package/dist/domain/partners.js +43 -0
  27. package/dist/domain/partners.js.map +1 -0
  28. package/dist/domain/partners.test.d.ts +1 -0
  29. package/dist/domain/partners.test.js +53 -0
  30. package/dist/domain/partners.test.js.map +1 -0
  31. package/dist/domain/pools.d.ts +70 -0
  32. package/dist/domain/pools.js +134 -0
  33. package/dist/domain/pools.js.map +1 -0
  34. package/dist/domain/pools.test.d.ts +1 -0
  35. package/dist/domain/pools.test.js +184 -0
  36. package/dist/domain/pools.test.js.map +1 -0
  37. package/dist/domain/rates.d.ts +83 -0
  38. package/dist/domain/rates.js +120 -0
  39. package/dist/domain/rates.js.map +1 -0
  40. package/dist/domain/rates.test.d.ts +1 -0
  41. package/dist/domain/rates.test.js +181 -0
  42. package/dist/domain/rates.test.js.map +1 -0
  43. package/dist/domain/requests.d.ts +181 -0
  44. package/dist/domain/requests.js +202 -0
  45. package/dist/domain/requests.js.map +1 -0
  46. package/dist/domain/requests.test.d.ts +1 -0
  47. package/dist/domain/requests.test.js +470 -0
  48. package/dist/domain/requests.test.js.map +1 -0
  49. package/dist/domain/settlement.d.ts +97 -0
  50. package/dist/domain/settlement.js +117 -0
  51. package/dist/domain/settlement.js.map +1 -0
  52. package/dist/domain/settlement.test.d.ts +1 -0
  53. package/dist/domain/settlement.test.js +152 -0
  54. package/dist/domain/settlement.test.js.map +1 -0
  55. package/dist/domain/tranche-display-name.d.ts +17 -0
  56. package/dist/domain/tranche-display-name.js +30 -0
  57. package/dist/domain/tranche-display-name.js.map +1 -0
  58. package/dist/domain/tranche-display-name.test.d.ts +1 -0
  59. package/dist/domain/tranche-display-name.test.js +58 -0
  60. package/dist/domain/tranche-display-name.test.js.map +1 -0
  61. package/dist/domain/tranches.d.ts +89 -0
  62. package/dist/domain/tranches.js +150 -0
  63. package/dist/domain/tranches.js.map +1 -0
  64. package/dist/domain/tranches.test.d.ts +1 -0
  65. package/dist/domain/tranches.test.js +206 -0
  66. package/dist/domain/tranches.test.js.map +1 -0
  67. package/dist/domain/wallet-errors.d.ts +37 -0
  68. package/dist/domain/wallet-errors.js +56 -0
  69. package/dist/domain/wallet-errors.js.map +1 -0
  70. package/dist/domain/wallet-errors.test.d.ts +1 -0
  71. package/dist/domain/wallet-errors.test.js +71 -0
  72. package/dist/domain/wallet-errors.test.js.map +1 -0
  73. package/dist/facade/chain-configs.js +61 -1
  74. package/dist/facade/chain-configs.js.map +1 -1
  75. package/dist/facade/config.test.d.ts +1 -0
  76. package/dist/facade/config.test.js +216 -0
  77. package/dist/facade/config.test.js.map +1 -0
  78. package/dist/facade/deposits.d.ts +22 -1
  79. package/dist/facade/deposits.js +24 -1
  80. package/dist/facade/deposits.js.map +1 -1
  81. package/dist/facade/facade.test.d.ts +1 -0
  82. package/dist/{tests → facade}/facade.test.js +87 -5
  83. package/dist/facade/facade.test.js.map +1 -0
  84. package/dist/facade/index.d.ts +2 -1
  85. package/dist/facade/index.js +2 -0
  86. package/dist/facade/index.js.map +1 -1
  87. package/dist/facade/kasu.d.ts +49 -8
  88. package/dist/facade/kasu.js +117 -13
  89. package/dist/facade/kasu.js.map +1 -1
  90. package/dist/facade/strategies.d.ts +25 -0
  91. package/dist/facade/strategies.js +36 -0
  92. package/dist/facade/strategies.js.map +1 -1
  93. package/dist/facade/types.d.ts +57 -2
  94. package/dist/facade/unused-pool-ids.d.ts +31 -0
  95. package/dist/facade/unused-pool-ids.js +57 -0
  96. package/dist/facade/unused-pool-ids.js.map +1 -0
  97. package/dist/facade/user-portfolio.d.ts +18 -0
  98. package/dist/facade/user-portfolio.js +23 -0
  99. package/dist/facade/user-portfolio.js.map +1 -1
  100. package/dist/index.d.ts +2 -0
  101. package/dist/index.js +6 -0
  102. package/dist/index.js.map +1 -1
  103. package/dist/sdk-config.d.ts +11 -0
  104. package/dist/sdk-config.js +8 -1
  105. package/dist/sdk-config.js.map +1 -1
  106. package/dist/services/DataService/data-service.d.ts +5 -0
  107. package/dist/services/DataService/data-service.js +10 -12
  108. package/dist/services/DataService/data-service.js.map +1 -1
  109. package/dist/services/DataService/directus-client.d.ts +26 -0
  110. package/dist/services/DataService/directus-client.js +38 -0
  111. package/dist/services/DataService/directus-client.js.map +1 -0
  112. package/dist/services/Locking/calculate-apy.test.d.ts +1 -0
  113. package/dist/services/Locking/calculate-apy.test.js +41 -0
  114. package/dist/services/Locking/calculate-apy.test.js.map +1 -0
  115. package/dist/services/Locking/locking.d.ts +14 -0
  116. package/dist/services/Locking/locking.js +16 -2
  117. package/dist/services/Locking/locking.js.map +1 -1
  118. package/dist/services/UserLending/user-lending.js +11 -7
  119. package/dist/services/UserLending/user-lending.js.map +1 -1
  120. package/dist/tests/sample.test.js +46 -25
  121. package/dist/tests/sample.test.js.map +1 -1
  122. package/package.json +43 -12
  123. package/src/domain/au-minimum.test.ts +371 -0
  124. package/src/domain/au-minimum.ts +192 -0
  125. package/src/domain/deposit-bounds.test.ts +200 -0
  126. package/src/domain/deposit-bounds.ts +118 -0
  127. package/src/domain/index.ts +134 -0
  128. package/src/domain/loan-contract.test.ts +343 -0
  129. package/src/domain/loan-contract.ts +275 -0
  130. package/src/domain/partners.test.ts +83 -0
  131. package/src/domain/partners.ts +66 -0
  132. package/src/domain/pools.test.ts +260 -0
  133. package/src/domain/pools.ts +150 -0
  134. package/src/domain/rates.test.ts +254 -0
  135. package/src/domain/rates.ts +120 -0
  136. package/src/domain/requests.test.ts +653 -0
  137. package/src/domain/requests.ts +414 -0
  138. package/src/domain/settlement.test.ts +198 -0
  139. package/src/domain/settlement.ts +161 -0
  140. package/src/domain/tranche-display-name.test.ts +96 -0
  141. package/src/domain/tranche-display-name.ts +34 -0
  142. package/src/domain/tranches.test.ts +300 -0
  143. package/src/domain/tranches.ts +177 -0
  144. package/src/domain/wallet-errors.test.ts +100 -0
  145. package/src/domain/wallet-errors.ts +56 -0
  146. package/src/facade/chain-configs.ts +62 -1
  147. package/src/facade/config.test.ts +265 -0
  148. package/src/facade/deposits.ts +25 -0
  149. package/src/{tests → facade}/facade.test.ts +134 -5
  150. package/src/facade/index.ts +4 -0
  151. package/src/facade/kasu.ts +155 -12
  152. package/src/facade/strategies.ts +36 -0
  153. package/src/facade/types.ts +60 -2
  154. package/src/facade/unused-pool-ids.ts +59 -0
  155. package/src/facade/user-portfolio.ts +24 -0
  156. package/src/index.ts +8 -0
  157. package/src/sdk-config.ts +19 -1
  158. package/src/services/DataService/data-service.ts +14 -26
  159. package/src/services/DataService/directus-client.ts +54 -0
  160. package/src/services/Locking/calculate-apy.test.ts +52 -0
  161. package/src/services/Locking/locking.ts +16 -2
  162. package/src/services/UserLending/user-lending.ts +17 -21
  163. package/src/tests/sample.test.ts +54 -33
  164. package/.eslintignore +0 -3
  165. package/.eslintrc.json +0 -90
  166. package/.github/workflows/ci.yml +0 -39
  167. package/.github/workflows/gitleaks.yml +0 -22
  168. package/.github/workflows/main.yml +0 -41
  169. package/.gitleaksignore +0 -19
  170. package/.prettierignore +0 -3
  171. package/.prettierrc +0 -28
  172. package/CLAUDE.md +0 -410
  173. package/copy-abi.ps1 +0 -15
  174. package/dist/tests/facade.test.js.map +0 -1
  175. package/index.ts +0 -2
  176. package/jest-config.ts +0 -9
  177. package/jest.config.js +0 -8
  178. package/rollup.config.js +0 -45
  179. package/tsconfig.json +0 -27
  180. /package/dist/{tests/facade.test.d.ts → domain/au-minimum.test.d.ts} +0 -0
@@ -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 @@
1
+ export {};
@@ -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,40 @@
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.
12
+ *
13
+ * Two kinds of string are exempt, and only these two. `getTrancheDisplayName`
14
+ * is here precisely because that rename must NOT drift between apps. And
15
+ * `loan-contract.ts` builds PROTOCOL strings — messages kasu-backend
16
+ * reconstructs byte-for-byte to verify a lender's signature. Those are not
17
+ * copy: nobody may reword them, in any language, without a matching backend
18
+ * change, which is exactly why they belong in one place.
19
+ * 2. **Pure.** No network, no clock, no environment. Anything with I/O belongs
20
+ * in `facade/` (see `fetchUnusedPoolIds`).
21
+ */
22
+ export { apyToEpochRate, epochRateToApy, EPOCHS_IN_YEAR, netEffectiveApy, } from './rates';
23
+ export { AU_ALPHA3, AU_MIN_CUMULATIVE_BY_STABLE, auMinimumRemaining, auThresholdFor, isAuMinimumExempt, isAustralianKyc, parseMinorUnits, } from './au-minimum';
24
+ export type { AuMinimumInput } from './au-minimum';
25
+ export { ceilToCents, floorToCents, isBelowMinimumCapacity, MAX_LENDING_AMOUNT_FALLBACK, MIN_LENDING_AMOUNT_FALLBACK, parseTrancheBound, resolveBoundShortcuts, resolveDepositBounds, } from './deposit-bounds';
26
+ export type { BoundShortcuts, DepositBounds } from './deposit-bounds';
27
+ export { APXIUM, getCreditOriginator, getInstitutionalLender, INVOICEMATE, RIXON_CAPITAL, } from './partners';
28
+ export type { PoolNameSignal, StrategyPartner } from './partners';
29
+ export { asContractType, buildContractVersionType, buildFullNameRequestMessage, buildLegacyContractRequestMessage, buildLoanAgreementSignMessage, encodeDepositData, formatSignTimestampUtc, parseFormattedMessage, } from './loan-contract';
30
+ export type { ContractListItem, ContractSection, ContractType, ExemptLoanContract, GenerateContractResponse, LoanContractFormatted, ResolvedContractResponse, RetailLoanContract, } from './loan-contract';
31
+ export { maxNetRateCeiling, pickHighestYieldTranche, poolMaxApy, selectVisiblePools, } from './pools';
32
+ export type { BestTranche } from './pools';
33
+ export { countSubmissions, deriveRequestState, firstSubmissionTimestamp, isCycleClosed, submissionEvents, } from './requests';
34
+ export type { RequestKind, RequestState, RequestStatusCode, } from './requests';
35
+ export { CLEARING_WINDOW_SECONDS, computeSettlementWindow, deriveCycleDates, nextCycleBoundary, } from './settlement';
36
+ export type { CycleDates, SettlementWindowInput, SettlementWindowState, } from './settlement';
37
+ export { getTrancheDisplayName, UPPER_MEZZANINE } from './tranche-display-name';
38
+ export { compareTrancheSeniority, derivePoolStatus, MIN_TRANCHE_CAPACITY, netTrancheApyBounds, pickDefaultTrancheId, poolAllTranchesFull, trancheApyBounds, trancheHasCapacity, trancheRiskRank, } from './tranches';
39
+ export type { ApyBounds, PoolStatus, TrancheCapacitySignal, } from './tranches';
40
+ export { isUnpredictableGas, isUserRejected } from './wallet-errors';
@@ -0,0 +1,33 @@
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.
12
+ *
13
+ * Two kinds of string are exempt, and only these two. `getTrancheDisplayName`
14
+ * is here precisely because that rename must NOT drift between apps. And
15
+ * `loan-contract.ts` builds PROTOCOL strings — messages kasu-backend
16
+ * reconstructs byte-for-byte to verify a lender's signature. Those are not
17
+ * copy: nobody may reword them, in any language, without a matching backend
18
+ * change, which is exactly why they belong in one place.
19
+ * 2. **Pure.** No network, no clock, no environment. Anything with I/O belongs
20
+ * in `facade/` (see `fetchUnusedPoolIds`).
21
+ */
22
+ export { apyToEpochRate, epochRateToApy, EPOCHS_IN_YEAR, netEffectiveApy, } from './rates';
23
+ export { AU_ALPHA3, AU_MIN_CUMULATIVE_BY_STABLE, auMinimumRemaining, auThresholdFor, isAuMinimumExempt, isAustralianKyc, parseMinorUnits, } from './au-minimum';
24
+ export { ceilToCents, floorToCents, isBelowMinimumCapacity, MAX_LENDING_AMOUNT_FALLBACK, MIN_LENDING_AMOUNT_FALLBACK, parseTrancheBound, resolveBoundShortcuts, resolveDepositBounds, } from './deposit-bounds';
25
+ export { APXIUM, getCreditOriginator, getInstitutionalLender, INVOICEMATE, RIXON_CAPITAL, } from './partners';
26
+ export { asContractType, buildContractVersionType, buildFullNameRequestMessage, buildLegacyContractRequestMessage, buildLoanAgreementSignMessage, encodeDepositData, formatSignTimestampUtc, parseFormattedMessage, } from './loan-contract';
27
+ export { maxNetRateCeiling, pickHighestYieldTranche, poolMaxApy, selectVisiblePools, } from './pools';
28
+ export { countSubmissions, deriveRequestState, firstSubmissionTimestamp, isCycleClosed, submissionEvents, } from './requests';
29
+ export { CLEARING_WINDOW_SECONDS, computeSettlementWindow, deriveCycleDates, nextCycleBoundary, } from './settlement';
30
+ export { getTrancheDisplayName, UPPER_MEZZANINE } from './tranche-display-name';
31
+ export { compareTrancheSeniority, derivePoolStatus, MIN_TRANCHE_CAPACITY, netTrancheApyBounds, pickDefaultTrancheId, poolAllTranchesFull, trancheApyBounds, trancheHasCapacity, trancheRiskRank, } from './tranches';
32
+ export { isUnpredictableGas, isUserRejected } from './wallet-errors';
33
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/domain/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,EACH,cAAc,EACd,cAAc,EACd,cAAc,EACd,eAAe,GAClB,MAAM,SAAS,CAAC;AAEjB,OAAO,EACH,SAAS,EACT,2BAA2B,EAC3B,kBAAkB,EAClB,cAAc,EACd,iBAAiB,EACjB,eAAe,EACf,eAAe,GAClB,MAAM,cAAc,CAAC;AAGtB,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,cAAc,EACd,wBAAwB,EACxB,2BAA2B,EAC3B,iCAAiC,EACjC,6BAA6B,EAC7B,iBAAiB,EACjB,sBAAsB,EACtB,qBAAqB,GACxB,MAAM,iBAAiB,CAAC;AAYzB,OAAO,EACH,iBAAiB,EACjB,uBAAuB,EACvB,UAAU,EACV,kBAAkB,GACrB,MAAM,SAAS,CAAC;AAGjB,OAAO,EACH,gBAAgB,EAChB,kBAAkB,EAClB,wBAAwB,EACxB,aAAa,EACb,gBAAgB,GACnB,MAAM,YAAY,CAAC;AAOpB,OAAO,EACH,uBAAuB,EACvB,uBAAuB,EACvB,gBAAgB,EAChB,iBAAiB,GACpB,MAAM,cAAc,CAAC;AAOtB,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;AAOpB,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC"}
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Loan-contract protocol: the strings and the bytes kasu-backend verifies.
3
+ *
4
+ * ⚠️ THE ONE EXCEPTION TO "NUMBERS AND CODES ONLY". Everything this module
5
+ * returns as a string is a PROTOCOL string, not copy. The backend reconstructs
6
+ * each of these messages byte-for-byte and verifies the lender's signature
7
+ * against it; a changed word, separator, line break or date format does not
8
+ * read differently — it stops every signature verifying. They are here, in the
9
+ * shared layer, for exactly the reason `getTrancheDisplayName` is: so the
10
+ * applications cannot drift apart on them.
11
+ *
12
+ * Any change to a builder below is a coordinated multi-repo change that has to
13
+ * land in kasu-backend at the same moment. Do not "tidy" this file.
14
+ *
15
+ * Lifted from kasu-ui's `features/lending/lib/{contract-types, sign-message}.ts`
16
+ * and `encode-deposit-data.ts` — the last rewritten from viem to ethers v5
17
+ * (kasu-mobile already runs that port; the tests pin the two byte-identical).
18
+ */
19
+ /** Recursive list nesting: `list-N-list`, etc. — indexed by string keys. */
20
+ export type ContractListItem = {
21
+ label?: string;
22
+ description?: string;
23
+ } & Record<string, unknown>;
24
+ export type ContractSection = {
25
+ title?: string;
26
+ description?: string;
27
+ } & Record<`list-${number}`, ContractListItem | undefined> & Record<string, unknown>;
28
+ export type RetailLoanContract = {
29
+ important?: {
30
+ title?: string;
31
+ description?: string;
32
+ };
33
+ intro?: string;
34
+ between?: string;
35
+ parties?: ContractSection;
36
+ background?: ContractSection;
37
+ witnesses?: ContractSection;
38
+ } & Record<`subheader-${number}`, ContractSection | undefined> & Record<string, unknown>;
39
+ /**
40
+ * The exempt (wholesale) contract renders from the same tree as the retail
41
+ * one; the templates differ, the SHAPE does not. Kept as its own name because
42
+ * `contractType` distinguishes them everywhere else.
43
+ */
44
+ export type ExemptLoanContract = RetailLoanContract;
45
+ /** Either contract, parsed. Both are the same tree. */
46
+ export type LoanContractFormatted = RetailLoanContract;
47
+ /**
48
+ * `contractType` arrives over the wire as a free string; these are the two the
49
+ * version byte encodes.
50
+ */
51
+ export type ContractType = 'retail' | 'exempt';
52
+ export interface GenerateContractResponse {
53
+ fullName: string;
54
+ /** The plaintext the lender signs (EIP-191). */
55
+ contractMessage: string;
56
+ /** Returned as a JSON-encoded string; parse before rendering. */
57
+ formattedMessage: string;
58
+ contractType: ContractType;
59
+ /** Template version (>= 1). */
60
+ contractVersion: number;
61
+ /** ms-epoch; feeds the on-chain `depositData` and the TTL guard. */
62
+ timestamp: number;
63
+ }
64
+ export type ResolvedContractResponse = GenerateContractResponse & {
65
+ isValid: boolean;
66
+ };
67
+ /** Narrow the backend's loose `contractType` string to the encoded union. */
68
+ export declare function asContractType(raw: string): ContractType;
69
+ /**
70
+ * Parse the server's JSON-string `formattedMessage` into a tree. Returns
71
+ * `null` on parse failure so a renderer can fall back to the plaintext.
72
+ */
73
+ export declare function parseFormattedMessage(raw: string): LoanContractFormatted | null;
74
+ /**
75
+ * Pack the contract version and type into the `versionType` word.
76
+ *
77
+ * ```
78
+ * high byte = contract version (>= 1)
79
+ * low byte = 0 for retail, 1 for exempt
80
+ * ```
81
+ */
82
+ export declare function buildContractVersionType(contractVersion: number, contractType: ContractType): number;
83
+ /**
84
+ * Build the on-chain `depositData` blob that `requestDepositWithKyc` expects.
85
+ *
86
+ * The KasuController decodes the bytes as
87
+ * `(bytes signature, uint256 timestamp, uint256 versionType)` and uses the
88
+ * embedded acceptance signature to verify — retrospectively, via the
89
+ * agreements service `/contract/resolve` — that the lender signed the
90
+ * loan-contract text. The ABI tuple and the packing are consensus-critical:
91
+ * these bytes go on chain.
92
+ *
93
+ * kasu-ui encodes this with viem, kasu-mobile with ethers v5 (viem is not
94
+ * available on Expo). This is the ethers v5 implementation, and
95
+ * `loan-contract.test.ts` pins its output byte-for-byte against fixtures
96
+ * produced by the viem version, so the two apps can never diverge here.
97
+ *
98
+ * @param args.signature EIP-191 signature from the lender accepting
99
+ * `contractMessage`, as a 0x-prefixed hex string.
100
+ * @param args.timestamp ms-epoch from the contract response.
101
+ */
102
+ export declare function encodeDepositData(args: {
103
+ signature: string;
104
+ timestamp: number;
105
+ contractVersion: number;
106
+ contractType: ContractType;
107
+ }): string;
108
+ /**
109
+ * Format a unix timestamp as `{day} {MonthName} {yyyy}, {HH}:{mm}` in UTC.
110
+ * Day is non-padded; hour and minute are zero-padded to two digits (24h). A
111
+ * timestamp with >= 13 digits is treated as milliseconds, otherwise as seconds
112
+ * — the same auto-detection kasu-backend applies.
113
+ *
114
+ * Deliberately a manual formatter with English month names: no locale, no
115
+ * `Intl`, so the output is byte-identical across runtimes and time zones. This
116
+ * is not a display date. It goes inside a signed message.
117
+ *
118
+ * e.g. 1785313320000 → `"29 July 2026, 08:22"`
119
+ */
120
+ export declare function formatSignTimestampUtc(timestamp: number): string;
121
+ /**
122
+ * The 4-line human-readable message a lender signs to generate their loan
123
+ * agreement for review — `POST /contract/generate`.
124
+ *
125
+ * ⚠️ BYTE-EXACT PROTOCOL STRING. kasu-backend rebuilds this string from the
126
+ * request body and verifies the signature against it, so the wording,
127
+ * ordering, separators, line breaks and date format are all part of the wire
128
+ * contract. The separator between the line-2 fields is a MIDDLE DOT U+00B7
129
+ * (·) with a single space on each side; the four lines are joined with `\n`.
130
+ *
131
+ * The backend takes this format only when all four display fields are present
132
+ * and non-empty, and it cross-checks `amountLabel`'s leading number against
133
+ * the `depositAmount` it was sent (thousands separators stripped) — a message
134
+ * that states an amount other than the one being executed is refused.
135
+ */
136
+ export declare function buildLoanAgreementSignMessage(p: {
137
+ strategyName: string;
138
+ region: string;
139
+ optionName: string;
140
+ amountLabel: string;
141
+ timestamp: number;
142
+ }): string;
143
+ /**
144
+ * The legacy `/contract/generate` and `/contract/resolve` message.
145
+ *
146
+ * ⚠️ BYTE-EXACT PROTOCOL STRING. kasu-backend rebuilds it as
147
+ * `` `I request contract content for ${address} at ${timestamp}.` `` from the
148
+ * `address` and `timestamp` fields of the request body — so the string signed
149
+ * and the body sent must agree exactly, INCLUDING the address casing. This
150
+ * builder lowercases, and the request body must carry the same lowercased
151
+ * address; that is what both apps signing this format do today.
152
+ *
153
+ * The backend takes this path whenever the four human-readable display fields
154
+ * are absent, and documents it as permanent until the legacy app is
155
+ * decommissioned. `/contract/resolve` has no other format — every consumer
156
+ * signs this one to retrieve an existing agreement.
157
+ *
158
+ * @param timestampMs ms-epoch, and the same value sent as the body's
159
+ * `timestamp`.
160
+ */
161
+ export declare function buildLegacyContractRequestMessage(address: string, timestampMs: number): string;
162
+ /**
163
+ * The `POST /contract/fullname` message.
164
+ *
165
+ * ⚠️ BYTE-EXACT PROTOCOL STRING, on the same terms as
166
+ * `buildLegacyContractRequestMessage`: kasu-backend rebuilds
167
+ * `` `I request my full name for ${address} at ${timestamp}.` `` from the
168
+ * request body and verifies the signature against it, so the body must carry
169
+ * the same lowercased address this builder signs.
170
+ *
171
+ * @param timestampMs ms-epoch, and the same value sent as the body's
172
+ * `timestamp`.
173
+ */
174
+ export declare function buildFullNameRequestMessage(address: string, timestampMs: number): string;
@@ -0,0 +1,160 @@
1
+ import { ethers } from 'ethers';
2
+ /** Narrow the backend's loose `contractType` string to the encoded union. */
3
+ export function asContractType(raw) {
4
+ return raw === 'exempt' ? 'exempt' : 'retail';
5
+ }
6
+ /**
7
+ * Parse the server's JSON-string `formattedMessage` into a tree. Returns
8
+ * `null` on parse failure so a renderer can fall back to the plaintext.
9
+ */
10
+ export function parseFormattedMessage(raw) {
11
+ try {
12
+ const parsed = JSON.parse(raw);
13
+ if (parsed && typeof parsed === 'object')
14
+ return parsed;
15
+ return null;
16
+ }
17
+ catch (_a) {
18
+ return null;
19
+ }
20
+ }
21
+ // ---------------------------------------------------------------------------
22
+ // The on-chain `depositData` blob
23
+ // ---------------------------------------------------------------------------
24
+ /**
25
+ * Pack the contract version and type into the `versionType` word.
26
+ *
27
+ * ```
28
+ * high byte = contract version (>= 1)
29
+ * low byte = 0 for retail, 1 for exempt
30
+ * ```
31
+ */
32
+ export function buildContractVersionType(contractVersion, contractType) {
33
+ return (contractVersion << 8) + (contractType === 'retail' ? 0 : 1);
34
+ }
35
+ /**
36
+ * Build the on-chain `depositData` blob that `requestDepositWithKyc` expects.
37
+ *
38
+ * The KasuController decodes the bytes as
39
+ * `(bytes signature, uint256 timestamp, uint256 versionType)` and uses the
40
+ * embedded acceptance signature to verify — retrospectively, via the
41
+ * agreements service `/contract/resolve` — that the lender signed the
42
+ * loan-contract text. The ABI tuple and the packing are consensus-critical:
43
+ * these bytes go on chain.
44
+ *
45
+ * kasu-ui encodes this with viem, kasu-mobile with ethers v5 (viem is not
46
+ * available on Expo). This is the ethers v5 implementation, and
47
+ * `loan-contract.test.ts` pins its output byte-for-byte against fixtures
48
+ * produced by the viem version, so the two apps can never diverge here.
49
+ *
50
+ * @param args.signature EIP-191 signature from the lender accepting
51
+ * `contractMessage`, as a 0x-prefixed hex string.
52
+ * @param args.timestamp ms-epoch from the contract response.
53
+ */
54
+ export function encodeDepositData(args) {
55
+ const versionType = buildContractVersionType(args.contractVersion, args.contractType);
56
+ return ethers.utils.defaultAbiCoder.encode(['bytes', 'uint256', 'uint256'], [
57
+ args.signature,
58
+ ethers.BigNumber.from(args.timestamp),
59
+ ethers.BigNumber.from(versionType),
60
+ ]);
61
+ }
62
+ // ---------------------------------------------------------------------------
63
+ // The signed messages
64
+ // ---------------------------------------------------------------------------
65
+ const MONTH_NAMES = [
66
+ 'January',
67
+ 'February',
68
+ 'March',
69
+ 'April',
70
+ 'May',
71
+ 'June',
72
+ 'July',
73
+ 'August',
74
+ 'September',
75
+ 'October',
76
+ 'November',
77
+ 'December',
78
+ ];
79
+ /**
80
+ * Format a unix timestamp as `{day} {MonthName} {yyyy}, {HH}:{mm}` in UTC.
81
+ * Day is non-padded; hour and minute are zero-padded to two digits (24h). A
82
+ * timestamp with >= 13 digits is treated as milliseconds, otherwise as seconds
83
+ * — the same auto-detection kasu-backend applies.
84
+ *
85
+ * Deliberately a manual formatter with English month names: no locale, no
86
+ * `Intl`, so the output is byte-identical across runtimes and time zones. This
87
+ * is not a display date. It goes inside a signed message.
88
+ *
89
+ * e.g. 1785313320000 → `"29 July 2026, 08:22"`
90
+ */
91
+ export function formatSignTimestampUtc(timestamp) {
92
+ const ms = timestamp.toString().length >= 13 ? timestamp : timestamp * 1000;
93
+ const date = new Date(ms);
94
+ const day = date.getUTCDate();
95
+ const month = MONTH_NAMES[date.getUTCMonth()];
96
+ const year = date.getUTCFullYear();
97
+ const hours = String(date.getUTCHours()).padStart(2, '0');
98
+ const minutes = String(date.getUTCMinutes()).padStart(2, '0');
99
+ return `${day} ${month} ${year}, ${hours}:${minutes}`;
100
+ }
101
+ /**
102
+ * The 4-line human-readable message a lender signs to generate their loan
103
+ * agreement for review — `POST /contract/generate`.
104
+ *
105
+ * ⚠️ BYTE-EXACT PROTOCOL STRING. kasu-backend rebuilds this string from the
106
+ * request body and verifies the signature against it, so the wording,
107
+ * ordering, separators, line breaks and date format are all part of the wire
108
+ * contract. The separator between the line-2 fields is a MIDDLE DOT U+00B7
109
+ * (·) with a single space on each side; the four lines are joined with `\n`.
110
+ *
111
+ * The backend takes this format only when all four display fields are present
112
+ * and non-empty, and it cross-checks `amountLabel`'s leading number against
113
+ * the `depositAmount` it was sent (thousands separators stripped) — a message
114
+ * that states an amount other than the one being executed is refused.
115
+ */
116
+ export function buildLoanAgreementSignMessage(p) {
117
+ return [
118
+ 'Generate my Loan Agreement for review:',
119
+ `${p.strategyName} · ${p.region} · ${p.optionName} · ${p.amountLabel}.`,
120
+ `Request made ${formatSignTimestampUtc(p.timestamp)} UTC.`,
121
+ 'This request does not commit me to lend.',
122
+ ].join('\n');
123
+ }
124
+ /**
125
+ * The legacy `/contract/generate` and `/contract/resolve` message.
126
+ *
127
+ * ⚠️ BYTE-EXACT PROTOCOL STRING. kasu-backend rebuilds it as
128
+ * `` `I request contract content for ${address} at ${timestamp}.` `` from the
129
+ * `address` and `timestamp` fields of the request body — so the string signed
130
+ * and the body sent must agree exactly, INCLUDING the address casing. This
131
+ * builder lowercases, and the request body must carry the same lowercased
132
+ * address; that is what both apps signing this format do today.
133
+ *
134
+ * The backend takes this path whenever the four human-readable display fields
135
+ * are absent, and documents it as permanent until the legacy app is
136
+ * decommissioned. `/contract/resolve` has no other format — every consumer
137
+ * signs this one to retrieve an existing agreement.
138
+ *
139
+ * @param timestampMs ms-epoch, and the same value sent as the body's
140
+ * `timestamp`.
141
+ */
142
+ export function buildLegacyContractRequestMessage(address, timestampMs) {
143
+ return `I request contract content for ${address.toLowerCase()} at ${timestampMs}.`;
144
+ }
145
+ /**
146
+ * The `POST /contract/fullname` message.
147
+ *
148
+ * ⚠️ BYTE-EXACT PROTOCOL STRING, on the same terms as
149
+ * `buildLegacyContractRequestMessage`: kasu-backend rebuilds
150
+ * `` `I request my full name for ${address} at ${timestamp}.` `` from the
151
+ * request body and verifies the signature against it, so the body must carry
152
+ * the same lowercased address this builder signs.
153
+ *
154
+ * @param timestampMs ms-epoch, and the same value sent as the body's
155
+ * `timestamp`.
156
+ */
157
+ export function buildFullNameRequestMessage(address, timestampMs) {
158
+ return `I request my full name for ${address.toLowerCase()} at ${timestampMs}.`;
159
+ }
160
+ //# sourceMappingURL=loan-contract.js.map