@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,177 @@
1
+ import { PoolOverview, TrancheData } from '../services/DataService/types';
2
+
3
+ import { netEffectiveApy } from './rates';
4
+
5
+ /**
6
+ * Minimum remaining capacity (in stable-asset units) for a tranche to be
7
+ * selectable. Mirrors `MIN_CAPACITY = toBigNumber('1')` in kasu-fe-next's
8
+ * lending modal after its `formatEther`. Anything below this is rounding
9
+ * noise — the SDK's own capacity arithmetic routinely leaves sub-cent dust
10
+ * behind.
11
+ */
12
+ export const MIN_TRANCHE_CAPACITY = 1;
13
+
14
+ /**
15
+ * Remaining capacity as it reaches this layer. `TrancheData` (raw, from the
16
+ * data service) calls it `poolCapacity`; the facade's `StrategyTranche` calls
17
+ * the same number `availableCapacity`. Accepting both keeps ONE capacity gate
18
+ * for the whole SDK rather than a duplicate per shape.
19
+ */
20
+ export type TrancheCapacitySignal =
21
+ | { poolCapacity: string }
22
+ | { availableCapacity: string };
23
+
24
+ export function trancheHasCapacity(tranche: TrancheCapacitySignal): boolean {
25
+ const raw =
26
+ 'poolCapacity' in tranche
27
+ ? tranche.poolCapacity
28
+ : tranche.availableCapacity;
29
+ const remaining = parseFloat(raw);
30
+ return Number.isFinite(remaining) && remaining >= MIN_TRANCHE_CAPACITY;
31
+ }
32
+
33
+ /**
34
+ * Whole-strategy capacity gate: true when the pool has tranches but none of
35
+ * them has remaining capacity. Drives a lend form's full-capacity message and
36
+ * its step-counter suppression.
37
+ */
38
+ export function poolAllTranchesFull(pool: PoolOverview): boolean {
39
+ return pool.tranches.length > 0 && !pool.tranches.some(trancheHasCapacity);
40
+ }
41
+
42
+ export type PoolStatus = 'Live' | 'Full' | 'Coming soon';
43
+
44
+ /**
45
+ * The status CODE for a pool. A code, not copy: consumers map it to their own
46
+ * wording and locale.
47
+ */
48
+ export function derivePoolStatus(pool: PoolOverview): PoolStatus {
49
+ if (!pool.enabled) return 'Coming soon';
50
+ if (poolAllTranchesFull(pool)) return 'Full';
51
+ return 'Live';
52
+ }
53
+
54
+ /**
55
+ * Seniority rank of a tranche by name — LOWER is less risky. The credit
56
+ * waterfall is Senior (repaid first, lowest risk) → Mezzanine → Junior
57
+ * (repaid last, highest risk). APY is NOT a reliable proxy (a pool can price
58
+ * its Senior tranche above its Mezzanine), so risk is ranked by name.
59
+ * Unrecognised names sort last, so a known-safe tranche always wins the
60
+ * default. Ranks by the RAW subgraph name — the Apxium "Upper Mezzanine"
61
+ * rename is display-only and must never reach here (see
62
+ * `tranche-display-name.ts`).
63
+ */
64
+ const TRANCHE_RISK_RANK: Record<string, number | undefined> = {
65
+ senior: 0,
66
+ mezzanine: 1,
67
+ junior: 2,
68
+ };
69
+
70
+ // `name` is typed `string` on `TrancheData` but arrives from the subgraph, so
71
+ // it is taken as possibly missing here: an unnamed tranche ranks last rather
72
+ // than throwing on `.trim()`.
73
+ function riskRankOfName(name: string | undefined): number {
74
+ return (
75
+ TRANCHE_RISK_RANK[name?.trim().toLowerCase() ?? ''] ??
76
+ Number.POSITIVE_INFINITY
77
+ );
78
+ }
79
+
80
+ export function trancheRiskRank(tranche: TrancheData): number {
81
+ return riskRankOfName(tranche.name);
82
+ }
83
+
84
+ /**
85
+ * Comparator that orders tranches SAFEST-FIRST (Senior → Mezzanine → Junior)
86
+ * by the same name-based risk rank the default-pick uses. Ranks by the RAW
87
+ * subgraph name, so the Apxium "Upper Mezzanine" display rename never reaches
88
+ * here. Ties (incl. unrecognised names, which both rank last) keep their input
89
+ * order under a stable sort. Used by a lend dropdown to list the least-risky
90
+ * option first.
91
+ */
92
+ export function compareTrancheSeniority(a: TrancheData, b: TrancheData): number {
93
+ const ra = trancheRiskRank(a);
94
+ const rb = trancheRiskRank(b);
95
+ if (ra === rb) return 0;
96
+ return ra < rb ? -1 : 1;
97
+ }
98
+
99
+ /**
100
+ * Capacity-aware default tranche pick: the LOWEST-risk tranche that still has
101
+ * capacity, so a new lender lands on the safest available option. Riskier
102
+ * tranches remain a deliberate opt-in — we must never pre-select the
103
+ * highest-risk Junior (audit 3.2). When every tranche is full we fall back to
104
+ * the lowest-risk one regardless, so a form still mounts in a known state
105
+ * (the dropdown marks it full and the submit gate blocks progress).
106
+ */
107
+ export function pickDefaultTrancheId(pool: PoolOverview): string {
108
+ const withCapacity = pool.tranches.filter(trancheHasCapacity);
109
+ const candidates = withCapacity.length > 0 ? withCapacity : pool.tranches;
110
+ if (candidates.length === 0) return '';
111
+ return candidates.reduce((best, t) =>
112
+ trancheRiskRank(t) < trancheRiskRank(best) ? t : best,
113
+ ).id;
114
+ }
115
+
116
+ /** A closed APY range, both bounds as 0..1 fractions. */
117
+ export interface ApyBounds {
118
+ min: number;
119
+ max: number;
120
+ }
121
+
122
+ /**
123
+ * GROSS APY range across a pool's tranches — the numeric core of kasu-ui's
124
+ * `formatTrancheApyRange`. Each `TrancheData` carries `minApy`/`maxApy`
125
+ * derived from the base rate plus any fixed-term configs (`data-service.ts`).
126
+ *
127
+ * Zero and negative values are skipped — an unset/missing rate (e.g. a tranche
128
+ * with no fixed-term config) must not drag the range down to `0–10.5%`.
129
+ * `null` when nothing usable remains, which is the caller's cue to render its
130
+ * "no rate" state rather than a confident zero.
131
+ *
132
+ * The min is taken over `minApy` and the max over `maxApy`, so a single
133
+ * unusable side collapses the whole range to `null` rather than half a range.
134
+ *
135
+ * Formatting is NOT here. The rule that two bounds printing the same figure
136
+ * collapse to one is decided on the rendered digits, so it belongs with the
137
+ * formatter that produces them (kasu-ui `format-tranche-apy.ts`).
138
+ */
139
+ export function trancheApyBounds(tranches: TrancheData[]): ApyBounds | null {
140
+ if (tranches.length === 0) return null;
141
+ let lo = Number.POSITIVE_INFINITY;
142
+ let hi = Number.NEGATIVE_INFINITY;
143
+ for (const t of tranches) {
144
+ const min = parseFloat(t.minApy);
145
+ const max = parseFloat(t.maxApy);
146
+ if (Number.isFinite(min) && min > 0) lo = Math.min(lo, min);
147
+ if (Number.isFinite(max) && max > 0) hi = Math.max(hi, max);
148
+ }
149
+ if (!Number.isFinite(lo) || !Number.isFinite(hi)) return null;
150
+ return { min: lo, max: hi };
151
+ }
152
+
153
+ /**
154
+ * The same range as NET Effective Interest Rates — each bound through
155
+ * `netEffectiveApy`.
156
+ *
157
+ * FAIL CLOSED: `null` when either bound comes back non-finite or ≤ 0, so an
158
+ * out-of-domain fee or a rate-less pool can never be rendered as a confident
159
+ * figure. A caller that does not yet know the fee must not substitute `0` —
160
+ * a fee-less rate overstates the figure by up to 3.4pp with nothing on screen
161
+ * saying so; it should skip the call and render its unavailable state.
162
+ *
163
+ * @param feePercent the chain's performance fee, 0..100 — never a fraction.
164
+ * See `netEffectiveApy` for why the units matter.
165
+ */
166
+ export function netTrancheApyBounds(
167
+ tranches: TrancheData[],
168
+ feePercent: number,
169
+ ): ApyBounds | null {
170
+ const gross = trancheApyBounds(tranches);
171
+ if (!gross) return null;
172
+ const min = netEffectiveApy(gross.min, feePercent);
173
+ const max = netEffectiveApy(gross.max, feePercent);
174
+ if (!Number.isFinite(min) || min <= 0) return null;
175
+ if (!Number.isFinite(max) || max <= 0) return null;
176
+ return { min, max };
177
+ }
@@ -0,0 +1,100 @@
1
+ import { isUnpredictableGas, isUserRejected } from './wallet-errors';
2
+
3
+ /**
4
+ * `isUserRejected` cases ported from kasu-ui
5
+ * `src/lib/web3/is-user-rejected.test.ts`. `isUnpredictableGas` had no test in
6
+ * kasu-mobile; one is written here.
7
+ */
8
+
9
+ describe('isUserRejected', () => {
10
+ it('detects EIP-1193 numeric code 4001', () => {
11
+ expect(isUserRejected({ code: 4001, message: 'whatever' })).toBe(true);
12
+ });
13
+
14
+ it('detects ACTION_REJECTED string code (ethers v5)', () => {
15
+ expect(
16
+ isUserRejected({ code: 'ACTION_REJECTED', message: 'rejected' }),
17
+ ).toBe(true);
18
+ });
19
+
20
+ it('detects "User rejected" message string', () => {
21
+ expect(isUserRejected(new Error('User rejected the request'))).toBe(
22
+ true,
23
+ );
24
+ });
25
+
26
+ it('detects "User denied" message string', () => {
27
+ expect(
28
+ isUserRejected(new Error('User denied transaction signature')),
29
+ ).toBe(true);
30
+ });
31
+
32
+ it('detects the ethers ACTION_REJECTED marker in a message', () => {
33
+ expect(
34
+ isUserRejected(new Error('transaction failed: ACTION_REJECTED')),
35
+ ).toBe(true);
36
+ });
37
+
38
+ it('is case-insensitive', () => {
39
+ expect(isUserRejected(new Error('user REJECTED the request'))).toBe(
40
+ true,
41
+ );
42
+ });
43
+
44
+ it('returns false for unrelated errors', () => {
45
+ expect(isUserRejected(new Error('Network error'))).toBe(false);
46
+ expect(isUserRejected(new Error('Insufficient funds'))).toBe(false);
47
+ });
48
+
49
+ it('returns false for null / undefined', () => {
50
+ expect(isUserRejected(null)).toBe(false);
51
+ expect(isUserRejected(undefined)).toBe(false);
52
+ });
53
+
54
+ it('handles non-Error throwables', () => {
55
+ expect(isUserRejected('user rejected')).toBe(true);
56
+ expect(isUserRejected('something else')).toBe(false);
57
+ });
58
+
59
+ it('is not fooled by a numeric code that only looks like 4001', () => {
60
+ expect(isUserRejected({ code: '4001' })).toBe(false);
61
+ expect(isUserRejected({ code: 4002 })).toBe(false);
62
+ });
63
+ });
64
+
65
+ describe('isUnpredictableGas', () => {
66
+ it('detects the ethers v5 gas-estimation revert code', () => {
67
+ expect(isUnpredictableGas({ code: 'UNPREDICTABLE_GAS_LIMIT' })).toBe(
68
+ true,
69
+ );
70
+ });
71
+
72
+ it('does not fire on a user rejection', () => {
73
+ expect(isUnpredictableGas({ code: 'ACTION_REJECTED' })).toBe(false);
74
+ expect(isUnpredictableGas({ code: 4001 })).toBe(false);
75
+ });
76
+
77
+ it('does not fire on the code appearing only in a message', () => {
78
+ // The code is the signal; a message mentioning it is not one, or a
79
+ // logged error string would be mistaken for a revert.
80
+ expect(
81
+ isUnpredictableGas(new Error('UNPREDICTABLE_GAS_LIMIT')),
82
+ ).toBe(false);
83
+ expect(isUnpredictableGas('UNPREDICTABLE_GAS_LIMIT')).toBe(false);
84
+ });
85
+
86
+ it('returns false for null / undefined and for unrelated errors', () => {
87
+ expect(isUnpredictableGas(null)).toBe(false);
88
+ expect(isUnpredictableGas(undefined)).toBe(false);
89
+ expect(isUnpredictableGas(new Error('Network error'))).toBe(false);
90
+ });
91
+
92
+ it('reads the code off an ethers error object', () => {
93
+ const err = Object.assign(new Error('cannot estimate gas'), {
94
+ code: 'UNPREDICTABLE_GAS_LIMIT',
95
+ reason: 'execution reverted: ERC20: insufficient allowance',
96
+ });
97
+ expect(isUnpredictableGas(err)).toBe(true);
98
+ expect(isUserRejected(err)).toBe(false);
99
+ });
100
+ });
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Wallet and RPC error predicates — pure functions over `unknown`.
3
+ *
4
+ * Every consumer has to tell three things apart when a write fails: the lender
5
+ * changed their mind, the call would revert, and everything else. The first
6
+ * two must never be reported as a failure the lender should retry or contact
7
+ * support about, and each wallet spells them differently, so the shapes are
8
+ * enumerated once here.
9
+ *
10
+ * Lifted from kasu-ui's `src/lib/web3/is-user-rejected.ts` and kasu-mobile's
11
+ * `src/features/lending/lib/errors.ts`.
12
+ */
13
+
14
+ /**
15
+ * Did the lender reject the request in their wallet?
16
+ *
17
+ * Providers surface a rejection in different shapes:
18
+ * - MetaMask and most EIP-1193 wallets: `code: 4001`
19
+ * - WalletConnect and some Privy paths: `Error('User rejected the request')`
20
+ * - ethers v5 wraps it as `ACTION_REJECTED`
21
+ * - Coinbase Wallet sometimes: `code: 'ACTION_REJECTED'` as a string
22
+ *
23
+ * This is kasu-ui's implementation verbatim. kasu-mobile's copy additionally
24
+ * reads a nested `error.code` / `error.message`, an ethers `reason`, and the
25
+ * words "request rejected" / "declined"; a consumer that needs those shapes
26
+ * should keep its own check on top rather than assume they are covered here.
27
+ */
28
+ export function isUserRejected(err: unknown): boolean {
29
+ if (!err) return false;
30
+ if (typeof err === 'object') {
31
+ const code = (err as { code?: unknown }).code;
32
+ if (code === 4001 || code === 'ACTION_REJECTED') return true;
33
+ }
34
+ const msg = err instanceof Error ? err.message : String(err);
35
+ const lower = msg.toLowerCase();
36
+ return (
37
+ lower.includes('user rejected') ||
38
+ lower.includes('user denied') ||
39
+ lower.includes('rejected the request') ||
40
+ lower.includes('action_rejected')
41
+ );
42
+ }
43
+
44
+ /**
45
+ * Did the wallet or RPC signal that the on-chain call would revert?
46
+ *
47
+ * ethers v5 raises `UNPREDICTABLE_GAS_LIMIT` when gas estimation reverts —
48
+ * most often an underlying `transferFrom` failing on an insufficient balance
49
+ * or allowance. Distinct from a rejection: nothing was refused by the lender,
50
+ * the transaction simply cannot succeed as composed, so the caller should
51
+ * re-check its preconditions rather than invite a retry.
52
+ */
53
+ export function isUnpredictableGas(err: unknown): boolean {
54
+ if (!err || typeof err !== 'object') return false;
55
+ return (err as { code?: unknown }).code === 'UNPREDICTABLE_GAS_LIMIT';
56
+ }
@@ -1,5 +1,20 @@
1
1
  import { ChainConfigEntry } from './types';
2
2
 
3
+ /**
4
+ * Browser-viable XDC RPCs only. `rpc.xdc.org` / `erpc.xdc.org` answer OPTIONS
5
+ * without `access-control-allow-origin`, so a browser call hangs on preflight
6
+ * — they must never appear here. The four below ship CORS `*` and are DISTINCT
7
+ * operators, so one vendor incident cannot exhaust the list. Order is a
8
+ * STARTING preference only; consumers are expected to fold the list into their
9
+ * own failover. Shared by both XDC deployments (same chain, same endpoints).
10
+ */
11
+ const XDC_RPC_URLS: string[] = [
12
+ 'https://rpc.ankr.com/xdc',
13
+ 'https://rpc.xdcrpc.com',
14
+ 'https://rpc.xinfin.network',
15
+ 'https://rpc.primenumbers.xyz/',
16
+ ];
17
+
3
18
  /**
4
19
  * Built-in chain configurations for Kasu-supported networks.
5
20
  *
@@ -33,6 +48,17 @@ export const CHAIN_CONFIGS: Record<'base' | 'xdc' | 'xdc-usdc' | 'plume', ChainC
33
48
  directusUrl: 'https://kasu-finance.directus.app/',
34
49
  unusedPoolIds: [],
35
50
  poolMetadataMapping: undefined,
51
+ stableAsset: {
52
+ address: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
53
+ symbol: 'USDC',
54
+ name: 'USD Coin',
55
+ decimals: 6,
56
+ currencyCode: 'USD',
57
+ },
58
+ rpcUrls: [
59
+ 'https://base-rpc.publicnode.com',
60
+ 'https://mainnet.base.org',
61
+ ],
36
62
  },
37
63
 
38
64
  xdc: {
@@ -65,6 +91,14 @@ export const CHAIN_CONFIGS: Record<'base' | 'xdc' | 'xdc-usdc' | 'plume', ChainC
65
91
  '0xeda50c91a8c4ca8a83652b8542c0b3bd00a71fad':
66
92
  '0xc347a9e4aec8c8d11a149d2907deb2bf23b81c6f',
67
93
  },
94
+ stableAsset: {
95
+ address: '0x9fe4e6321eeb7c4bc537570f015e4734b15002b8',
96
+ symbol: 'AUDD',
97
+ name: 'Novatti Australian Dollar',
98
+ decimals: 6,
99
+ currencyCode: 'AUD',
100
+ },
101
+ rpcUrls: XDC_RPC_URLS,
68
102
  },
69
103
 
70
104
  'xdc-usdc': {
@@ -90,6 +124,14 @@ export const CHAIN_CONFIGS: Record<'base' | 'xdc' | 'xdc-usdc' | 'plume', ChainC
90
124
  directusUrl: 'https://kasu-finance.directus.app/',
91
125
  unusedPoolIds: [],
92
126
  poolMetadataMapping: undefined,
127
+ stableAsset: {
128
+ address: '0xfa2958cb79b0491cc627c1557f441ef849ca8eb1',
129
+ symbol: 'USDC',
130
+ name: 'USD Coin',
131
+ decimals: 6,
132
+ currencyCode: 'USD',
133
+ },
134
+ rpcUrls: XDC_RPC_URLS,
93
135
  },
94
136
 
95
137
  plume: {
@@ -109,10 +151,29 @@ export const CHAIN_CONFIGS: Record<'base' | 'xdc' | 'xdc-usdc' | 'plume', ChainC
109
151
  ClearingCoordinator: '',
110
152
  ExternalTVL: '',
111
153
  },
154
+ // The frozen Plume history is indexed on the LEGACY Goldsky project,
155
+ // not the one the live chains use: the same path under the current
156
+ // project 404s. Verified 2026-09-05 — this URL answers
157
+ // `{ lendingPools { id name } }` with the three Plume pools; the
158
+ // current-project spelling returns HTTP 404. Note the `/gn` suffix,
159
+ // which the current project's URLs do not carry.
112
160
  subgraphUrl:
113
- 'https://api.goldsky.com/api/public/project_cmgzlpxm300765np2a19421om/subgraphs/kasu-plume/prod',
161
+ 'https://api.goldsky.com/api/public/project_cm9t3064xeuyn01tgctdo3c17/subgraphs/kasu-plume/prod/gn',
114
162
  directusUrl: 'https://kasu-finance.directus.app/',
115
163
  unusedPoolIds: [],
116
164
  poolMetadataMapping: undefined,
165
+ stableAsset: {
166
+ address: '0xdddD73F5Df1F0DC31373357beAC77545dC5A6f3F',
167
+ symbol: 'pUSD',
168
+ name: 'Plume USD',
169
+ decimals: 6,
170
+ currencyCode: 'USD',
171
+ },
172
+ // Retired — drained and wound down. No default RPC, so a read-only
173
+ // `Kasu.create({ chain: 'plume' })` throws rather than quietly
174
+ // pointing at an endpoint nobody maintains. Pass your own
175
+ // `signerOrProvider` to read the frozen history.
176
+ rpcUrls: [],
177
+ retired: true,
117
178
  },
118
179
  };