@kasufinance/kasu-sdk 2.5.0 → 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 (72) hide show
  1. package/README.md +17 -5
  2. package/dist/bundle.cjs.js +797 -16
  3. package/dist/bundle.esm.js +771 -17
  4. package/dist/domain/au-minimum.d.ts +135 -0
  5. package/dist/domain/au-minimum.js +154 -0
  6. package/dist/domain/au-minimum.js.map +1 -0
  7. package/dist/domain/au-minimum.test.d.ts +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/index.d.ts +17 -3
  11. package/dist/domain/index.js +13 -3
  12. package/dist/domain/index.js.map +1 -1
  13. package/dist/domain/loan-contract.d.ts +174 -0
  14. package/dist/domain/loan-contract.js +160 -0
  15. package/dist/domain/loan-contract.js.map +1 -0
  16. package/dist/domain/loan-contract.test.d.ts +1 -0
  17. package/dist/domain/loan-contract.test.js +255 -0
  18. package/dist/domain/loan-contract.test.js.map +1 -0
  19. package/dist/domain/requests.d.ts +181 -0
  20. package/dist/domain/requests.js +202 -0
  21. package/dist/domain/requests.js.map +1 -0
  22. package/dist/domain/requests.test.d.ts +1 -0
  23. package/dist/domain/requests.test.js +470 -0
  24. package/dist/domain/requests.test.js.map +1 -0
  25. package/dist/domain/settlement.d.ts +97 -0
  26. package/dist/domain/settlement.js +117 -0
  27. package/dist/domain/settlement.js.map +1 -0
  28. package/dist/domain/settlement.test.d.ts +1 -0
  29. package/dist/domain/settlement.test.js +152 -0
  30. package/dist/domain/settlement.test.js.map +1 -0
  31. package/dist/domain/wallet-errors.d.ts +37 -0
  32. package/dist/domain/wallet-errors.js +56 -0
  33. package/dist/domain/wallet-errors.js.map +1 -0
  34. package/dist/domain/wallet-errors.test.d.ts +1 -0
  35. package/dist/domain/wallet-errors.test.js +71 -0
  36. package/dist/domain/wallet-errors.test.js.map +1 -0
  37. package/dist/facade/chain-configs.js +7 -1
  38. package/dist/facade/chain-configs.js.map +1 -1
  39. package/dist/facade/facade.test.js +82 -5
  40. package/dist/facade/facade.test.js.map +1 -1
  41. package/dist/facade/user-portfolio.d.ts +18 -0
  42. package/dist/facade/user-portfolio.js +23 -0
  43. package/dist/facade/user-portfolio.js.map +1 -1
  44. package/dist/index.d.ts +1 -0
  45. package/dist/index.js +1 -0
  46. package/dist/index.js.map +1 -1
  47. package/dist/services/DataService/data-service.js +3 -10
  48. package/dist/services/DataService/data-service.js.map +1 -1
  49. package/dist/services/DataService/directus-client.d.ts +26 -0
  50. package/dist/services/DataService/directus-client.js +38 -0
  51. package/dist/services/DataService/directus-client.js.map +1 -0
  52. package/dist/services/UserLending/user-lending.js +11 -7
  53. package/dist/services/UserLending/user-lending.js.map +1 -1
  54. package/package.json +1 -1
  55. package/src/domain/au-minimum.test.ts +371 -0
  56. package/src/domain/au-minimum.ts +192 -0
  57. package/src/domain/index.ts +67 -3
  58. package/src/domain/loan-contract.test.ts +343 -0
  59. package/src/domain/loan-contract.ts +275 -0
  60. package/src/domain/requests.test.ts +653 -0
  61. package/src/domain/requests.ts +414 -0
  62. package/src/domain/settlement.test.ts +198 -0
  63. package/src/domain/settlement.ts +161 -0
  64. package/src/domain/wallet-errors.test.ts +100 -0
  65. package/src/domain/wallet-errors.ts +56 -0
  66. package/src/facade/chain-configs.ts +7 -1
  67. package/src/facade/facade.test.ts +124 -0
  68. package/src/facade/user-portfolio.ts +24 -0
  69. package/src/index.ts +2 -0
  70. package/src/services/DataService/data-service.ts +7 -24
  71. package/src/services/DataService/directus-client.ts +54 -0
  72. package/src/services/UserLending/user-lending.ts +17 -21
@@ -15719,6 +15719,43 @@ function filterArray(array, id_in) {
15719
15719
  }
15720
15720
  }
15721
15721
 
15722
+ /**
15723
+ * The error a CMS-only call raises on a deployment configured without
15724
+ * Directus. Named so a caller can match on it rather than on the message.
15725
+ */
15726
+ const NO_DIRECTUS_URL_MESSAGE = 'Kasu: this call needs Directus, but the SDK was configured without a ' +
15727
+ '`directusUrl`. On-chain data (pools, tranches, positions, requests) ' +
15728
+ 'works without one; CMS content does not.';
15729
+ /**
15730
+ * Build the Directus client, or a stand-in that refuses clearly.
15731
+ *
15732
+ * `directusUrl` is documented optional, and most of the SDK genuinely does not
15733
+ * need it — pools, tranches, positions and request history all come from the
15734
+ * subgraph and the chain. But `createDirectus('')` throws `Invalid URL` inside
15735
+ * the constructor, so omitting the URL used to make the whole SDK
15736
+ * unconstructable rather than merely CMS-less.
15737
+ *
15738
+ * With no URL, the services skip Directus where they can degrade (pool
15739
+ * descriptions, images and Directus pool names simply do not appear, and the
15740
+ * raw subgraph names are used instead), and a call that exists ONLY to read
15741
+ * CMS content rejects with `NO_DIRECTUS_URL_MESSAGE` — a sentence that says
15742
+ * what to configure, rather than a `null` dereference thrown from inside a
15743
+ * vendor SDK.
15744
+ */
15745
+ function createDirectusClient(directusUrl) {
15746
+ if (directusUrl) {
15747
+ return le(directusUrl)
15748
+ .with(ne())
15749
+ .with(Zp());
15750
+ }
15751
+ const refuse = () => {
15752
+ throw new Error(NO_DIRECTUS_URL_MESSAGE);
15753
+ };
15754
+ // A stand-in, not a client: every entry point the services use goes
15755
+ // through `request`, so refusing there covers all of them.
15756
+ return { request: refuse };
15757
+ }
15758
+
15722
15759
  const getPoolOverviewQuery = (ids) => gql `
15723
15760
  query getAllPoolOverview($epochId: String!, $unusedPools: [String]!) {
15724
15761
  lendingPools(
@@ -15874,15 +15911,7 @@ class DataService {
15874
15911
  this._kasuConfig = _kasuConfig;
15875
15912
  this._externalTvlAbi = KasuPoolExternalTVLAbi__factory.connect(_kasuConfig.contracts.ExternalTVL, signerOrProvider);
15876
15913
  this._graph = new GraphQLClient(_kasuConfig.subgraphUrl);
15877
- if (_kasuConfig.directusUrl) {
15878
- this._directus = le(_kasuConfig.directusUrl)
15879
- .with(ne())
15880
- .with(Zp());
15881
- }
15882
- else {
15883
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
15884
- this._directus = null;
15885
- }
15914
+ this._directus = createDirectusClient(_kasuConfig.directusUrl);
15886
15915
  }
15887
15916
  getUrlFromFile(fileName) {
15888
15917
  return `${this._kasuConfig.directusUrl}assets/${fileName}`;
@@ -17136,9 +17165,7 @@ class UserLending {
17136
17165
  this._clearingCoordinatorAbi = IClearingCoordinatorAbi__factory.connect(_kasuConfig.contracts.ClearingCoordinator, signerOrProvider);
17137
17166
  this._systemVariablesAbi = ISystemVariablesAbi__factory.connect(_kasuConfig.contracts.SystemVariables, signerOrProvider);
17138
17167
  this._dataService = new DataService(_kasuConfig, signerOrProvider);
17139
- this._directus = le(_kasuConfig.directusUrl)
17140
- .with(ne())
17141
- .with(Zp());
17168
+ this._directus = createDirectusClient(_kasuConfig.directusUrl);
17142
17169
  }
17143
17170
  getUserTotalPendingAndActiveDepositedAmount(user) {
17144
17171
  return __awaiter(this, void 0, void 0, function* () {
@@ -17256,9 +17283,14 @@ class UserLending {
17256
17283
  unusedPools: this._kasuConfig.UNUSED_LENDING_POOL_IDS,
17257
17284
  epochId,
17258
17285
  }),
17259
- this._directus.request(bs('PoolOverview', {
17260
- fields: ['id', 'poolName', 'subheading'],
17261
- })),
17286
+ // Directus supplies the display pool NAME only; without it the
17287
+ // raw subgraph name is used, which is what the fallback below
17288
+ // already does for a pool with no CMS entry.
17289
+ this._kasuConfig.directusUrl
17290
+ ? this._directus.request(bs('PoolOverview', {
17291
+ fields: ['id', 'poolName', 'subheading'],
17292
+ }))
17293
+ : [],
17262
17294
  ]);
17263
17295
  const retn = [];
17264
17296
  for (const userRequest of subgraphResult.userRequests) {
@@ -24293,7 +24325,13 @@ const CHAIN_CONFIGS = {
24293
24325
  ClearingCoordinator: '',
24294
24326
  ExternalTVL: '',
24295
24327
  },
24296
- subgraphUrl: 'https://api.goldsky.com/api/public/project_cmgzlpxm300765np2a19421om/subgraphs/kasu-plume/prod',
24328
+ // The frozen Plume history is indexed on the LEGACY Goldsky project,
24329
+ // not the one the live chains use: the same path under the current
24330
+ // project 404s. Verified 2026-09-05 — this URL answers
24331
+ // `{ lendingPools { id name } }` with the three Plume pools; the
24332
+ // current-project spelling returns HTTP 404. Note the `/gn` suffix,
24333
+ // which the current project's URLs do not carry.
24334
+ subgraphUrl: 'https://api.goldsky.com/api/public/project_cm9t3064xeuyn01tgctdo3c17/subgraphs/kasu-plume/prod/gn',
24297
24335
  directusUrl: 'https://kasu-finance.directus.app/',
24298
24336
  unusedPoolIds: [],
24299
24337
  poolMetadataMapping: undefined,
@@ -24833,6 +24871,214 @@ class StrategiesFacade {
24833
24871
  }
24834
24872
  }
24835
24873
 
24874
+ var UserRequestStatus;
24875
+ (function (UserRequestStatus) {
24876
+ UserRequestStatus["REQUESTED"] = "Requested";
24877
+ UserRequestStatus["PROCESSING"] = "Processing";
24878
+ UserRequestStatus["PROCESSED"] = "Processed";
24879
+ })(UserRequestStatus || (UserRequestStatus = {}));
24880
+
24881
+ /**
24882
+ * Events that represent a lender SUBMISSION into the bundle. A dNFT position
24883
+ * aggregates every submission the lender made into the same pool/tranche this
24884
+ * cycle: the first is `Initiated`, each subsequent top-up is `Increased`.
24885
+ * Everything else on the timeline (Accepted / Rejected / Cancelled /
24886
+ * Reallocated / Forced) is an OUTCOME, not a request, and must not be counted.
24887
+ */
24888
+ const SUBMISSION_EVENTS = new Set(['Initiated', 'Increased']);
24889
+ /**
24890
+ * The submissions bundled into one dNFT-aggregate request row, input order
24891
+ * preserved — one loan agreement per submission.
24892
+ */
24893
+ function submissionEvents(events) {
24894
+ return events.filter((e) => SUBMISSION_EVENTS.has(e.requestType));
24895
+ }
24896
+ /** Count the submissions bundled into one dNFT-aggregate request row. */
24897
+ function countSubmissions(events) {
24898
+ return submissionEvents(events).length;
24899
+ }
24900
+ /**
24901
+ * Timestamp of the FIRST submission in the bundle. Falls back to `fallback`
24902
+ * when the `Initiated` event has not indexed yet — the caller decides what
24903
+ * that is (kasu-ui passes the request's own timestamp).
24904
+ */
24905
+ function firstSubmissionTimestamp(events, fallback) {
24906
+ const submissions = submissionEvents(events);
24907
+ if (submissions.length === 0)
24908
+ return fallback;
24909
+ return submissions.reduce((min, e) => (e.timestamp < min ? e.timestamp : min), Infinity);
24910
+ }
24911
+ /**
24912
+ * Has THIS request's cycle closed? — the single open/closed signal behind the
24913
+ * status vocabulary and behind Cancel.
24914
+ *
24915
+ * `request.canCancel` is the SDK's `isCancelable(status, poolId)` —
24916
+ * `status !== 'Processed' && !isLendingPoolClearingPending(pool)`. It is
24917
+ * per-POOL and reads the same condition the contract enforces on the cancel
24918
+ * call — but it reads it ONCE, when the request was fetched. Nothing about
24919
+ * this value is live, so a client that holds a request across a cycle close
24920
+ * must refetch before acting on it.
24921
+ *
24922
+ * The raw subgraph `status` must NOT feed this: `'Processing'` is a STICKY
24923
+ * historical marker set on the first partial fill and never reset, so gating
24924
+ * on it would freeze a partly-filled request in Processing forever. A global
24925
+ * settlement clock is equally wrong here — it is blind to whether THIS
24926
+ * request's pool is already clearing.
24927
+ */
24928
+ function isCycleClosed(request) {
24929
+ return !request.canCancel;
24930
+ }
24931
+ const isCancelled = (events) => events.some((e) => e.requestType === 'Cancelled');
24932
+ const isForced = (events) => events.some((e) => e.requestType === 'Forced');
24933
+ const initiatedEvent = (events) => events.find((e) => e.requestType === 'Initiated');
24934
+ /**
24935
+ * Subgraph behaviour: when a request is cancelled, the on-request
24936
+ * `requestedAmount` field is reset to 0 (the lender's effective balance is
24937
+ * restored). The original amount survives on the `Initiated` event's
24938
+ * `assetAmount`. Recover from there so cancelled rows still carry the amount
24939
+ * the lender originally asked for.
24940
+ */
24941
+ const initiatedAmountOf = (events) => {
24942
+ const initiated = initiatedEvent(events);
24943
+ return initiated ? Number(initiated.assetAmount || '0') : 0;
24944
+ };
24945
+ /**
24946
+ * A deposit is REALLOCATED when the timeline carries a `Reallocated` event, or
24947
+ * an `Accepted` event into a tranche other than the one requested.
24948
+ */
24949
+ const findReallocation = (events, originalTrancheId) => events.find((e) => e.requestType === 'Reallocated' ||
24950
+ (e.requestType === 'Accepted' &&
24951
+ e.trancheId.toLowerCase() !==
24952
+ originalTrancheId.toLowerCase()));
24953
+ /** kasu-ui's `num`: an absent or unparseable figure reads as 0. */
24954
+ const num = (str) => {
24955
+ const n = Number(str !== null && str !== void 0 ? str : '0');
24956
+ return Number.isFinite(n) ? n : 0;
24957
+ };
24958
+ /** The same parse, keeping "the subgraph reported nothing" distinct from 0. */
24959
+ const numOrNull = (str) => {
24960
+ if (str === undefined || str === null || str.trim() === '')
24961
+ return null;
24962
+ const n = Number(str);
24963
+ return Number.isFinite(n) ? n : null;
24964
+ };
24965
+ /**
24966
+ * Convert a `UserRequest` into a `RequestState`. Pure — no clock, no network,
24967
+ * no copy.
24968
+ *
24969
+ * BRANCH ORDER (this IS the behaviour; it reproduces kasu-ui's
24970
+ * `deriveTransactionView` check for check):
24971
+ *
24972
+ * 1. a `Cancelled` event → `cancelled`, neutral, 0
24973
+ * 2. a withdrawal with a `Forced` event → `forced`, outflow, −accepted
24974
+ * 3. a reallocated deposit → `reallocated`, inflow, +accepted
24975
+ * 4. a withdrawal partly filled with a LIVE
24976
+ * remainder (cycle still open) → `partial`, outflow, −accepted
24977
+ * 5. resolved (`status === 'Processed'`):
24978
+ * withdrawal, partly filled → `partial`, outflow, −accepted
24979
+ * withdrawal, fully filled → `complete`, outflow, −accepted
24980
+ * deposit, nothing accepted → `rejected`, neutral, 0
24981
+ * deposit, part rejected → `partial`, inflow, +accepted
24982
+ * deposit, fully accepted → `complete`, inflow, +accepted
24983
+ * 6. otherwise → `pending`, ±requested
24984
+ *
24985
+ * `cancelled` is reachable ONLY from branch 1 — a Cancelled EVENT. No
24986
+ * processing or resolved state can derive it.
24987
+ *
24988
+ * Branch 4 is checked BEFORE the resolved branch and gates on `isCycleClosed`
24989
+ * (i.e. `canCancel`), never on the sticky raw status: a withdrawal that was
24990
+ * partly filled returns to the queue with a live Cancel, and reading the raw
24991
+ * status would strand it.
24992
+ *
24993
+ * WHAT THE APPLICATION STILL OWNS: the status word and the detail line beneath
24994
+ * it; the tranche display rename (`getTrancheDisplayName` on `trancheName`,
24995
+ * and on the reallocation destination read off `request.events`); the pool-name
24996
+ * split; the amount format. The "view loan agreement" affordance is a fact, and
24997
+ * it follows from two fields already here —
24998
+ * `requestType === 'Deposit' && statusCode !== 'cancelled' && statusCode !== 'rejected'`
24999
+ * — because neither a cancelled nor a fully-rejected deposit ever issued one,
25000
+ * and withdrawals sign no agreement at all.
25001
+ */
25002
+ function deriveRequestState(request) {
25003
+ var _a, _b, _c;
25004
+ const isWithdrawal = request.requestType === 'Withdrawal';
25005
+ const cancelled = isCancelled(request.events);
25006
+ // Cancelled requests have `requestedAmount` zeroed on the request itself;
25007
+ // pull the original value from the Initiated event so the row still
25008
+ // carries "100 cancelled" instead of "0 cancelled".
25009
+ const requested = cancelled
25010
+ ? num(request.requestedAmount) || initiatedAmountOf(request.events)
25011
+ : num(request.requestedAmount);
25012
+ const accepted = num(request.acceptedAmount);
25013
+ const rejected = num(request.rejectedAmount);
25014
+ const forced = isWithdrawal && isForced(request.events);
25015
+ const reallocation = !isWithdrawal && findReallocation(request.events, request.trancheId);
25016
+ const initiated = initiatedEvent(request.events);
25017
+ const base = {
25018
+ id: request.id,
25019
+ contractId: (_a = initiated === null || initiated === void 0 ? void 0 : initiated.id) !== null && _a !== void 0 ? _a : '',
25020
+ poolId: request.lendingPool.id.toLowerCase(),
25021
+ poolName: request.lendingPool.name,
25022
+ trancheName: request.trancheName,
25023
+ trancheId: request.trancheId,
25024
+ fixedTermConfigId: (_c = (_b = request.fixedTermConfig) === null || _b === void 0 ? void 0 : _b.configId) !== null && _c !== void 0 ? _c : '0',
25025
+ requestType: request.requestType,
25026
+ rawStatus: request.status,
25027
+ requestedAmount: requested,
25028
+ acceptedAmount: numOrNull(request.acceptedAmount),
25029
+ initiatedAmount: initiated
25030
+ ? initiatedAmountOf(request.events)
25031
+ : null,
25032
+ submissionCount: countSubmissions(request.events),
25033
+ // `firstSubmissionTimestamp` needs a fallback it will never use here:
25034
+ // the bundle is non-empty on every path that reaches the call.
25035
+ firstSubmissionTimestamp: countSubmissions(request.events) === 0
25036
+ ? null
25037
+ : firstSubmissionTimestamp(request.events, 0),
25038
+ cycleClosed: isCycleClosed(request),
25039
+ canCancel: request.canCancel,
25040
+ };
25041
+ // 1. Cancelled wins over everything — the ONLY path to `cancelled`.
25042
+ if (cancelled) {
25043
+ return Object.assign(Object.assign({}, base), { kind: 'neutral', amount: 0, statusCode: 'cancelled' });
25044
+ }
25045
+ // 2. Forced withdrawal (credit originator returned funds early).
25046
+ if (forced) {
25047
+ return Object.assign(Object.assign({}, base), { kind: 'outflow', amount: -accepted, statusCode: 'forced' });
25048
+ }
25049
+ // 3. Reallocated deposit (accepted into a different lending option).
25050
+ if (reallocation) {
25051
+ return Object.assign(Object.assign({}, base), { kind: 'inflow', amount: accepted, statusCode: 'reallocated' });
25052
+ }
25053
+ // 4. Withdrawal partly filled with a LIVE remainder: the request returns
25054
+ // to the queue with a live Cancel. Checked BEFORE the resolved branch and
25055
+ // via `isCycleClosed`, never via the sticky raw status.
25056
+ if (isWithdrawal &&
25057
+ accepted > 0 &&
25058
+ accepted < requested &&
25059
+ !isCycleClosed(request)) {
25060
+ return Object.assign(Object.assign({}, base), { kind: 'outflow', amount: -accepted, statusCode: 'partial' });
25061
+ }
25062
+ // 5. Resolved — a rejection IS a resolution.
25063
+ if (request.status === UserRequestStatus.PROCESSED) {
25064
+ if (isWithdrawal) {
25065
+ const partly = accepted < requested;
25066
+ return Object.assign(Object.assign({}, base), { kind: 'outflow', amount: -accepted, statusCode: partly ? 'partial' : 'complete' });
25067
+ }
25068
+ // Deposit: full reject vs partial vs full accept.
25069
+ if (accepted === 0) {
25070
+ return Object.assign(Object.assign({}, base), { kind: 'neutral', amount: 0, statusCode: 'rejected' });
25071
+ }
25072
+ if (rejected > 0) {
25073
+ return Object.assign(Object.assign({}, base), { kind: 'inflow', amount: accepted, statusCode: 'partial' });
25074
+ }
25075
+ return Object.assign(Object.assign({}, base), { kind: 'inflow', amount: accepted, statusCode: 'complete' });
25076
+ }
25077
+ // 6. Unresolved. `cycleClosed` tells the caller whether to render its
25078
+ // "queued" or its "processing" word; the code is the same either way.
25079
+ return Object.assign(Object.assign({}, base), { kind: isWithdrawal ? 'outflow' : 'inflow', amount: isWithdrawal ? -requested : requested, statusCode: 'pending' });
25080
+ }
25081
+
24836
25082
  /**
24837
25083
  * High-level facade for querying a user's portfolio, positions, and history.
24838
25084
  *
@@ -24875,6 +25121,28 @@ class PortfolioFacade {
24875
25121
  return yield this._userLending.getUserRequests(userAddress, currentEpoch);
24876
25122
  });
24877
25123
  }
25124
+ /**
25125
+ * The same history, already derived into `RequestState` rows — status
25126
+ * code, kind, signed amount, the cancelled-amount recovery, the bundled
25127
+ * submission count and the cycle-open signal.
25128
+ *
25129
+ * ```ts
25130
+ * const rows = await kasu.portfolio.getRequestStates('0xUser...');
25131
+ * rows.filter((r) => r.statusCode === 'pending');
25132
+ * ```
25133
+ *
25134
+ * Every application derives this from `getTransactionHistory` anyway, and
25135
+ * the derivation is the part they were each getting subtly differently.
25136
+ * Words are still the caller's: render `statusCode` in your own
25137
+ * vocabulary, and call `getTrancheDisplayName` on `trancheName` at the
25138
+ * view boundary.
25139
+ */
25140
+ getRequestStates(userAddress) {
25141
+ return __awaiter(this, void 0, void 0, function* () {
25142
+ const requests = yield this.getTransactionHistory(userAddress);
25143
+ return requests.map(deriveRequestState);
25144
+ });
25145
+ }
24878
25146
  }
24879
25147
 
24880
25148
  /**
@@ -25123,6 +25391,160 @@ function fetchUnusedPoolIds() {
25123
25391
  });
25124
25392
  }
25125
25393
 
25394
+ /**
25395
+ * The AU cumulative-lending minimum — the numeric half.
25396
+ *
25397
+ * RULE: a lender whose verified KYC country is Australia may only lend when
25398
+ * their existing deposited position on THIS deployment, plus the amount they
25399
+ * are asking for, reaches the deployment's threshold.
25400
+ *
25401
+ * This is UX PRE-VALIDATION ONLY. kasu-backend enforces the rule
25402
+ * authoritatively at contract generation and refuses with HTTP 403
25403
+ * `AU_WHOLESALE_MINIMUM_NOT_MET`; a lender who gets past this check is still
25404
+ * stopped there. What these helpers exist for is to raise the amount field's
25405
+ * minimum so the lender learns the rule while typing rather than at the end.
25406
+ *
25407
+ * FAIL-OPEN by design: an absent or unknown country is NOT restricted, and an
25408
+ * unlisted stable symbol has no threshold. An UNKNOWN existing position is
25409
+ * treated as 0 — strict, so the floor is never advertised lower than the
25410
+ * backend will accept, and it relaxes once the position loads.
25411
+ *
25412
+ * Arithmetic is integer bigint in minor units (10^decimals) so no two
25413
+ * consumers can disagree at the boundary. Parsing TRUNCATES, never rounds up —
25414
+ * a position can never be inflated into passing. `BigInt(...)` calls only, no
25415
+ * bigint literals, so consumers on an older target still compile.
25416
+ *
25417
+ * Lifted from kasu-ui's `features/lending/lib/au-lending-restriction.ts`. The
25418
+ * two toast strings that live there are copy and stay in the applications.
25419
+ */
25420
+ const ZERO = BigInt(0);
25421
+ /** ISO 3166-1 alpha-3 for Australia — the collapsed KYC country field. */
25422
+ const AU_ALPHA3 = 'AUS';
25423
+ /**
25424
+ * Is this the Australian KYC country? Case-insensitive and
25425
+ * whitespace-tolerant. Anything that is not a string — including the
25426
+ * `undefined` of an unloaded KYC record — is not restricted (fail-open).
25427
+ *
25428
+ * The alpha-2 `'AU'` deliberately does NOT match: the KYC country reaching
25429
+ * this rule is normalised to alpha-3 upstream, and a bare two-letter code
25430
+ * here means something else went wrong.
25431
+ */
25432
+ function isAustralianKyc(country) {
25433
+ if (typeof country !== 'string')
25434
+ return false;
25435
+ return country.trim().toUpperCase() === AU_ALPHA3;
25436
+ }
25437
+ /**
25438
+ * Minimum CUMULATIVE position, in WHOLE stable units, keyed on the
25439
+ * deployment's stable-asset symbol (a property of the currency, not the
25440
+ * chain). Must match kasu-backend's table exactly.
25441
+ */
25442
+ const AU_MIN_CUMULATIVE_BY_STABLE = {
25443
+ USDC: 360000,
25444
+ AUDD: 500000,
25445
+ };
25446
+ /**
25447
+ * Threshold in whole units, or `undefined` for an unlisted stable — a
25448
+ * deployment whose currency has no configured minimum is unrestricted.
25449
+ */
25450
+ function auThresholdFor(symbol) {
25451
+ if (typeof symbol !== 'string')
25452
+ return undefined;
25453
+ return AU_MIN_CUMULATIVE_BY_STABLE[symbol.trim().toUpperCase()];
25454
+ }
25455
+ /**
25456
+ * Is this wallet released from the minimum entirely, whatever its country,
25457
+ * stable or position says?
25458
+ *
25459
+ * `exemptAddresses` is DEPLOYMENT CONFIGURATION the caller supplies, not a
25460
+ * constant of this package. kasu-sdk is published publicly; the exempt list is
25461
+ * a compliance carve-out naming particular lender wallets, so it does not
25462
+ * belong in a public tarball. Read it from wherever the application keeps its
25463
+ * deployment configuration, and keep it identical to kasu-backend's
25464
+ * `AU_WHOLESALE_EXEMPT_ADDRESSES` — that is where the rule is actually
25465
+ * enforced. An address exempt here but not there sees no raised minimum in the
25466
+ * form and is refused at the end, which is worse than not exempting it at all.
25467
+ *
25468
+ * The address matched must be the CONNECTED wallet — the one that owns the KYC
25469
+ * record and signs the agreement request. kasu-backend verifies that signature
25470
+ * before the gate, so an exemption cannot be claimed by asserting someone
25471
+ * else's address; it takes their private key. Never match a "view as" address.
25472
+ *
25473
+ * Comparison is case-insensitive and whitespace-tolerant. Anything that is not
25474
+ * a string is NOT exempt (fail closed), and so is anything when the list is
25475
+ * absent.
25476
+ */
25477
+ function isAuMinimumExempt(address, exemptAddresses) {
25478
+ if (typeof address !== 'string' || !exemptAddresses)
25479
+ return false;
25480
+ const needle = address.trim().toLowerCase();
25481
+ return exemptAddresses.some((a) => a.trim().toLowerCase() === needle);
25482
+ }
25483
+ const DECIMAL_RE = /^(\d+)?(?:\.(\d*))?$/;
25484
+ /**
25485
+ * Truncating, partial-input-tolerant decimal → minor-unit parse.
25486
+ * `'10.'` → `10000000n` · `'1.23456789'` → `1234567n` (truncated at 6dp).
25487
+ * `''` / `'abc'` / negative / null → `null`.
25488
+ *
25489
+ * Tolerant of mid-typing states because it runs on an amount field as the
25490
+ * lender types, and truncating because rounding up would let a position that
25491
+ * is a fraction short read as passing.
25492
+ */
25493
+ function parseMinorUnits(value, decimals) {
25494
+ if (value === null || value === undefined)
25495
+ return null;
25496
+ const raw = typeof value === 'number' ? String(value) : value.trim();
25497
+ if (!raw)
25498
+ return null;
25499
+ const match = DECIMAL_RE.exec(raw);
25500
+ if (!match || (!match[1] && !match[2]))
25501
+ return null;
25502
+ // Both capture groups are optional, so either can be absent at runtime.
25503
+ // A fractional group that matched EMPTY ('10.') is not absent, and the
25504
+ // destructuring defaults leave it alone — only `undefined` takes them.
25505
+ const [, whole = '0', fracDigits = ''] = match;
25506
+ const frac = fracDigits.slice(0, decimals).padEnd(decimals, '0');
25507
+ return BigInt(whole + frac);
25508
+ }
25509
+ /**
25510
+ * The amount, in WHOLE stable units, an Australian lender still needs in order
25511
+ * to reach the deployment's cumulative minimum. Apply it as the amount field's
25512
+ * floor via `max(trancheMin, auMinimumRemaining(...))`.
25513
+ *
25514
+ * Returns 0 for everyone the rule does not restrict (fail-open), 0 for an
25515
+ * exempt address, and 0 once the lender's existing position already satisfies
25516
+ * the threshold.
25517
+ *
25518
+ * The exemption is tested FIRST and unconditionally: an exempt wallet has no
25519
+ * minimum whatever its country, stable or position says. kasu-backend
25520
+ * deliberately tests country first and the exemption second — the two orders
25521
+ * are not in conflict, both return "no minimum" for the same inputs. There,
25522
+ * the exemption sets a flag that drives a compliance log, so it must not fire
25523
+ * for a lender the rule never engaged for. Here nothing is logged, so
25524
+ * exemption-first is preferred: it makes this function right on its own,
25525
+ * whatever country a caller happens to pass.
25526
+ */
25527
+ function auMinimumRemaining(input) {
25528
+ var _a, _b;
25529
+ if (isAuMinimumExempt(input.address, input.exemptAddresses))
25530
+ return 0;
25531
+ if (!isAustralianKyc(input.country))
25532
+ return 0;
25533
+ const threshold = auThresholdFor(input.stableSymbol);
25534
+ if (threshold === undefined)
25535
+ return 0;
25536
+ const { existingDeposited, decimals } = input;
25537
+ // A table threshold always parses; `?? ZERO` keeps the rule fail-open
25538
+ // rather than asserting, so an unparseable one yields no minimum at all.
25539
+ const thresholdMinor = (_a = parseMinorUnits(threshold, decimals)) !== null && _a !== void 0 ? _a : ZERO;
25540
+ const existingMinor = (_b = parseMinorUnits(existingDeposited, decimals)) !== null && _b !== void 0 ? _b : ZERO;
25541
+ const remaining = thresholdMinor - existingMinor;
25542
+ if (remaining <= ZERO)
25543
+ return 0;
25544
+ const base = BigInt('1' + '0'.repeat(decimals));
25545
+ return Number(remaining / base) + Number(remaining % base) / Number(base);
25546
+ }
25547
+
25126
25548
  // Fallbacks if the selected tranche's on-chain min/max are 0 / NaN (some
25127
25549
  // "coming soon" pools haven't been configured yet).
25128
25550
  const MIN_LENDING_AMOUNT_FALLBACK = 500;
@@ -25250,6 +25672,282 @@ function getInstitutionalLender(originator) {
25250
25672
  return originator.name === APXIUM.name ? RIXON_CAPITAL : null;
25251
25673
  }
25252
25674
 
25675
+ /** Narrow the backend's loose `contractType` string to the encoded union. */
25676
+ function asContractType(raw) {
25677
+ return raw === 'exempt' ? 'exempt' : 'retail';
25678
+ }
25679
+ /**
25680
+ * Parse the server's JSON-string `formattedMessage` into a tree. Returns
25681
+ * `null` on parse failure so a renderer can fall back to the plaintext.
25682
+ */
25683
+ function parseFormattedMessage(raw) {
25684
+ try {
25685
+ const parsed = JSON.parse(raw);
25686
+ if (parsed && typeof parsed === 'object')
25687
+ return parsed;
25688
+ return null;
25689
+ }
25690
+ catch (_a) {
25691
+ return null;
25692
+ }
25693
+ }
25694
+ // ---------------------------------------------------------------------------
25695
+ // The on-chain `depositData` blob
25696
+ // ---------------------------------------------------------------------------
25697
+ /**
25698
+ * Pack the contract version and type into the `versionType` word.
25699
+ *
25700
+ * ```
25701
+ * high byte = contract version (>= 1)
25702
+ * low byte = 0 for retail, 1 for exempt
25703
+ * ```
25704
+ */
25705
+ function buildContractVersionType(contractVersion, contractType) {
25706
+ return (contractVersion << 8) + (contractType === 'retail' ? 0 : 1);
25707
+ }
25708
+ /**
25709
+ * Build the on-chain `depositData` blob that `requestDepositWithKyc` expects.
25710
+ *
25711
+ * The KasuController decodes the bytes as
25712
+ * `(bytes signature, uint256 timestamp, uint256 versionType)` and uses the
25713
+ * embedded acceptance signature to verify — retrospectively, via the
25714
+ * agreements service `/contract/resolve` — that the lender signed the
25715
+ * loan-contract text. The ABI tuple and the packing are consensus-critical:
25716
+ * these bytes go on chain.
25717
+ *
25718
+ * kasu-ui encodes this with viem, kasu-mobile with ethers v5 (viem is not
25719
+ * available on Expo). This is the ethers v5 implementation, and
25720
+ * `loan-contract.test.ts` pins its output byte-for-byte against fixtures
25721
+ * produced by the viem version, so the two apps can never diverge here.
25722
+ *
25723
+ * @param args.signature EIP-191 signature from the lender accepting
25724
+ * `contractMessage`, as a 0x-prefixed hex string.
25725
+ * @param args.timestamp ms-epoch from the contract response.
25726
+ */
25727
+ function encodeDepositData(args) {
25728
+ const versionType = buildContractVersionType(args.contractVersion, args.contractType);
25729
+ return ethers.utils.defaultAbiCoder.encode(['bytes', 'uint256', 'uint256'], [
25730
+ args.signature,
25731
+ ethers.BigNumber.from(args.timestamp),
25732
+ ethers.BigNumber.from(versionType),
25733
+ ]);
25734
+ }
25735
+ // ---------------------------------------------------------------------------
25736
+ // The signed messages
25737
+ // ---------------------------------------------------------------------------
25738
+ const MONTH_NAMES = [
25739
+ 'January',
25740
+ 'February',
25741
+ 'March',
25742
+ 'April',
25743
+ 'May',
25744
+ 'June',
25745
+ 'July',
25746
+ 'August',
25747
+ 'September',
25748
+ 'October',
25749
+ 'November',
25750
+ 'December',
25751
+ ];
25752
+ /**
25753
+ * Format a unix timestamp as `{day} {MonthName} {yyyy}, {HH}:{mm}` in UTC.
25754
+ * Day is non-padded; hour and minute are zero-padded to two digits (24h). A
25755
+ * timestamp with >= 13 digits is treated as milliseconds, otherwise as seconds
25756
+ * — the same auto-detection kasu-backend applies.
25757
+ *
25758
+ * Deliberately a manual formatter with English month names: no locale, no
25759
+ * `Intl`, so the output is byte-identical across runtimes and time zones. This
25760
+ * is not a display date. It goes inside a signed message.
25761
+ *
25762
+ * e.g. 1785313320000 → `"29 July 2026, 08:22"`
25763
+ */
25764
+ function formatSignTimestampUtc(timestamp) {
25765
+ const ms = timestamp.toString().length >= 13 ? timestamp : timestamp * 1000;
25766
+ const date = new Date(ms);
25767
+ const day = date.getUTCDate();
25768
+ const month = MONTH_NAMES[date.getUTCMonth()];
25769
+ const year = date.getUTCFullYear();
25770
+ const hours = String(date.getUTCHours()).padStart(2, '0');
25771
+ const minutes = String(date.getUTCMinutes()).padStart(2, '0');
25772
+ return `${day} ${month} ${year}, ${hours}:${minutes}`;
25773
+ }
25774
+ /**
25775
+ * The 4-line human-readable message a lender signs to generate their loan
25776
+ * agreement for review — `POST /contract/generate`.
25777
+ *
25778
+ * ⚠️ BYTE-EXACT PROTOCOL STRING. kasu-backend rebuilds this string from the
25779
+ * request body and verifies the signature against it, so the wording,
25780
+ * ordering, separators, line breaks and date format are all part of the wire
25781
+ * contract. The separator between the line-2 fields is a MIDDLE DOT U+00B7
25782
+ * (·) with a single space on each side; the four lines are joined with `\n`.
25783
+ *
25784
+ * The backend takes this format only when all four display fields are present
25785
+ * and non-empty, and it cross-checks `amountLabel`'s leading number against
25786
+ * the `depositAmount` it was sent (thousands separators stripped) — a message
25787
+ * that states an amount other than the one being executed is refused.
25788
+ */
25789
+ function buildLoanAgreementSignMessage(p) {
25790
+ return [
25791
+ 'Generate my Loan Agreement for review:',
25792
+ `${p.strategyName} · ${p.region} · ${p.optionName} · ${p.amountLabel}.`,
25793
+ `Request made ${formatSignTimestampUtc(p.timestamp)} UTC.`,
25794
+ 'This request does not commit me to lend.',
25795
+ ].join('\n');
25796
+ }
25797
+ /**
25798
+ * The legacy `/contract/generate` and `/contract/resolve` message.
25799
+ *
25800
+ * ⚠️ BYTE-EXACT PROTOCOL STRING. kasu-backend rebuilds it as
25801
+ * `` `I request contract content for ${address} at ${timestamp}.` `` from the
25802
+ * `address` and `timestamp` fields of the request body — so the string signed
25803
+ * and the body sent must agree exactly, INCLUDING the address casing. This
25804
+ * builder lowercases, and the request body must carry the same lowercased
25805
+ * address; that is what both apps signing this format do today.
25806
+ *
25807
+ * The backend takes this path whenever the four human-readable display fields
25808
+ * are absent, and documents it as permanent until the legacy app is
25809
+ * decommissioned. `/contract/resolve` has no other format — every consumer
25810
+ * signs this one to retrieve an existing agreement.
25811
+ *
25812
+ * @param timestampMs ms-epoch, and the same value sent as the body's
25813
+ * `timestamp`.
25814
+ */
25815
+ function buildLegacyContractRequestMessage(address, timestampMs) {
25816
+ return `I request contract content for ${address.toLowerCase()} at ${timestampMs}.`;
25817
+ }
25818
+ /**
25819
+ * The `POST /contract/fullname` message.
25820
+ *
25821
+ * ⚠️ BYTE-EXACT PROTOCOL STRING, on the same terms as
25822
+ * `buildLegacyContractRequestMessage`: kasu-backend rebuilds
25823
+ * `` `I request my full name for ${address} at ${timestamp}.` `` from the
25824
+ * request body and verifies the signature against it, so the body must carry
25825
+ * the same lowercased address this builder signs.
25826
+ *
25827
+ * @param timestampMs ms-epoch, and the same value sent as the body's
25828
+ * `timestamp`.
25829
+ */
25830
+ function buildFullNameRequestMessage(address, timestampMs) {
25831
+ return `I request my full name for ${address.toLowerCase()} at ${timestampMs}.`;
25832
+ }
25833
+
25834
+ /**
25835
+ * Cycles and the clearing window — the protocol's weekly clock, as numbers.
25836
+ *
25837
+ * Lifted verbatim from kasu-ui's `features/portfolio/lib/settlement-window.ts`
25838
+ * and `features/lending/lib/cycle-dates.ts`. The two formatters that live
25839
+ * beside `deriveCycleDates` there (`formatCycleDate`, `formatCycleCloseUtc`)
25840
+ * print words and stay in the applications; everything here is unix seconds in
25841
+ * and unix seconds out.
25842
+ *
25843
+ * The clearing window is the fixed 48 hours immediately preceding an epoch
25844
+ * end. Inside it, pending requests are being processed and cannot be modified,
25845
+ * and the countdown runs to the epoch end. Outside it, the countdown runs to
25846
+ * the next clearing-window start.
25847
+ *
25848
+ * The epoch end comes straight from the chain (`nextEpochStartTimestamp`, i.e.
25849
+ * the SDK's `getNextEpochDate`) — the same value the protocol's own
25850
+ * `getNextClearingPeriodDate` derives from — so the window always lines up
25851
+ * with the real weekly schedule (Tue 06:00 → Thu 06:00 UTC on Base) instead of
25852
+ * a projected subgraph timestamp that can drift off the grid.
25853
+ */
25854
+ /**
25855
+ * Clearing-window length — a fixed 48h protocol constant. Exported so every
25856
+ * consumer derives the window from the same number this module runs on.
25857
+ */
25858
+ const CLEARING_WINDOW_SECONDS = 48 * 60 * 60;
25859
+ /**
25860
+ * Weekly cadence — the epoch schedule is fixed weekly (Tue → Thu UTC on Base).
25861
+ * Used only to roll a cycle forward when a request lands inside a window that
25862
+ * has already closed.
25863
+ */
25864
+ const WEEK_SECONDS = 7 * 24 * 60 * 60;
25865
+ /**
25866
+ * Which phase of the weekly cycle `nowSeconds` falls in, and how long is left
25867
+ * of it.
25868
+ *
25869
+ * `'unknown'` when no epoch boundary has been loaded yet, or when the one on
25870
+ * hand is stale (it has already elapsed — the on-chain value refetches to the
25871
+ * next boundary shortly after rollover). A caller must render its "no cycle
25872
+ * loaded" state there, never a zeroed countdown.
25873
+ */
25874
+ function computeSettlementWindow({ nowSeconds, nextEpochStart, clearingWindowSeconds = CLEARING_WINDOW_SECONDS, }) {
25875
+ // No epoch boundary loaded yet, or a stale one that already elapsed.
25876
+ if (nextEpochStart <= 0 || nextEpochStart <= nowSeconds) {
25877
+ return { phase: 'unknown' };
25878
+ }
25879
+ const clearingStart = nextEpochStart - clearingWindowSeconds;
25880
+ if (nowSeconds < clearingStart) {
25881
+ return {
25882
+ phase: 'awaiting',
25883
+ secondsUntilClearing: clearingStart - nowSeconds,
25884
+ nextClearingStart: clearingStart,
25885
+ };
25886
+ }
25887
+ // nowSeconds is in [clearingStart, nextEpochStart) — inside the window.
25888
+ return {
25889
+ phase: 'clearing',
25890
+ secondsUntilEpochEnd: nextEpochStart - nowSeconds,
25891
+ epochEnd: nextEpochStart,
25892
+ };
25893
+ }
25894
+ /**
25895
+ * The next cycle boundary strictly after `nowSeconds`, in unix seconds — the
25896
+ * cycle close (`nextEpochStart − 48h`) while the window is still open, the
25897
+ * epoch end once we are inside it.
25898
+ *
25899
+ * `undefined` when there is no boundary left to wait for: no epoch boundary
25900
+ * loaded, or a cached one that has already elapsed — the same staleness rule
25901
+ * `computeSettlementWindow` applies before it reports `'unknown'`.
25902
+ *
25903
+ * Split out of the state machine because some consumers need the INSTANT
25904
+ * rather than the phase: one to flush the cycle-dependent caches when the
25905
+ * clock crosses it, one to move a pre-commit screen's snapshot clock at the
25906
+ * same moment.
25907
+ */
25908
+ function nextCycleBoundary(nextEpochStart, nowSeconds, clearingWindowSeconds = CLEARING_WINDOW_SECONDS) {
25909
+ if (!nextEpochStart || nextEpochStart <= 0)
25910
+ return undefined;
25911
+ const clearingStart = nextEpochStart - clearingWindowSeconds;
25912
+ if (nowSeconds < clearingStart)
25913
+ return clearingStart;
25914
+ if (nowSeconds < nextEpochStart)
25915
+ return nextEpochStart;
25916
+ return undefined;
25917
+ }
25918
+ /**
25919
+ * The cycle-close and outcome dates for a request submitted `now`.
25920
+ *
25921
+ * - The cycle "closes" (stops accepting requests, starts processing) at the
25922
+ * start of the 48h clearing window, i.e. 48h before the epoch end.
25923
+ * - Processing takes up to 48h, so the outcome is confirmed by the epoch end
25924
+ * (close + 48h).
25925
+ *
25926
+ * Returns `null` when the epoch boundary is not available or is stale — a
25927
+ * caller then omits the dates entirely (omit, don't stub).
25928
+ *
25929
+ * The common case is a request submitted OUTSIDE the clearing window: the
25930
+ * close is `nextEpochStart − 48h` and the outcome is `nextEpochStart`. When
25931
+ * the request lands INSIDE the current clearing window (that close is already
25932
+ * in the past), it queues for the NEXT weekly cycle, so the close is advanced
25933
+ * by whole weeks until it is in the future.
25934
+ */
25935
+ function deriveCycleDates(nextEpochStart, nowSeconds) {
25936
+ // No boundary loaded, or a stale one that already elapsed — the same
25937
+ // staleness rule as `computeSettlementWindow`.
25938
+ if (!nextEpochStart || nextEpochStart <= nowSeconds)
25939
+ return null;
25940
+ let close = nextEpochStart - CLEARING_WINDOW_SECONDS;
25941
+ let outcome = nextEpochStart;
25942
+ // Inside the current clearing window the close already passed; a request
25943
+ // now is queued for the next weekly cycle.
25944
+ while (close <= nowSeconds) {
25945
+ close += WEEK_SECONDS;
25946
+ outcome += WEEK_SECONDS;
25947
+ }
25948
+ return { close, outcome };
25949
+ }
25950
+
25253
25951
  // Business rename (2026-07): Apxium strategies market their top retail tranche
25254
25952
  // as "Upper Mezzanine" — the true senior position is held by the institutional
25255
25953
  // lender (Rixon Capital), so retail lenders are never actually senior in the
@@ -25279,4 +25977,60 @@ function getTrancheDisplayName(trancheName, pool) {
25279
25977
  : trancheName;
25280
25978
  }
25281
25979
 
25282
- export { APXIUM, CHAIN_CONFIGS, DepositsFacade, EPOCHS_IN_YEAR, INVOICEMATE, Kasu, KasuSdk, MAX_LENDING_AMOUNT_FALLBACK, MIN_LENDING_AMOUNT_FALLBACK, MIN_TRANCHE_CAPACITY, PortfolioFacade, RIXON_CAPITAL, SdkConfig, StrategiesFacade, UPPER_MEZZANINE, apyToEpochRate, ceilToCents, compareTrancheSeniority, derivePoolStatus, epochRateToApy, fetchUnusedPoolIds, floorToCents, getCreditOriginator, getInstitutionalLender, getTrancheDisplayName, isBelowMinimumCapacity, maxNetRateCeiling, netEffectiveApy, netTrancheApyBounds, parseTrancheBound, pickDefaultTrancheId, pickHighestYieldTranche, poolAllTranchesFull, poolMaxApy, resolveBoundShortcuts, resolveDepositBounds, selectVisiblePools, trancheApyBounds, trancheHasCapacity, trancheRiskRank };
25980
+ /**
25981
+ * Wallet and RPC error predicates — pure functions over `unknown`.
25982
+ *
25983
+ * Every consumer has to tell three things apart when a write fails: the lender
25984
+ * changed their mind, the call would revert, and everything else. The first
25985
+ * two must never be reported as a failure the lender should retry or contact
25986
+ * support about, and each wallet spells them differently, so the shapes are
25987
+ * enumerated once here.
25988
+ *
25989
+ * Lifted from kasu-ui's `src/lib/web3/is-user-rejected.ts` and kasu-mobile's
25990
+ * `src/features/lending/lib/errors.ts`.
25991
+ */
25992
+ /**
25993
+ * Did the lender reject the request in their wallet?
25994
+ *
25995
+ * Providers surface a rejection in different shapes:
25996
+ * - MetaMask and most EIP-1193 wallets: `code: 4001`
25997
+ * - WalletConnect and some Privy paths: `Error('User rejected the request')`
25998
+ * - ethers v5 wraps it as `ACTION_REJECTED`
25999
+ * - Coinbase Wallet sometimes: `code: 'ACTION_REJECTED'` as a string
26000
+ *
26001
+ * This is kasu-ui's implementation verbatim. kasu-mobile's copy additionally
26002
+ * reads a nested `error.code` / `error.message`, an ethers `reason`, and the
26003
+ * words "request rejected" / "declined"; a consumer that needs those shapes
26004
+ * should keep its own check on top rather than assume they are covered here.
26005
+ */
26006
+ function isUserRejected(err) {
26007
+ if (!err)
26008
+ return false;
26009
+ if (typeof err === 'object') {
26010
+ const code = err.code;
26011
+ if (code === 4001 || code === 'ACTION_REJECTED')
26012
+ return true;
26013
+ }
26014
+ const msg = err instanceof Error ? err.message : String(err);
26015
+ const lower = msg.toLowerCase();
26016
+ return (lower.includes('user rejected') ||
26017
+ lower.includes('user denied') ||
26018
+ lower.includes('rejected the request') ||
26019
+ lower.includes('action_rejected'));
26020
+ }
26021
+ /**
26022
+ * Did the wallet or RPC signal that the on-chain call would revert?
26023
+ *
26024
+ * ethers v5 raises `UNPREDICTABLE_GAS_LIMIT` when gas estimation reverts —
26025
+ * most often an underlying `transferFrom` failing on an insufficient balance
26026
+ * or allowance. Distinct from a rejection: nothing was refused by the lender,
26027
+ * the transaction simply cannot succeed as composed, so the caller should
26028
+ * re-check its preconditions rather than invite a retry.
26029
+ */
26030
+ function isUnpredictableGas(err) {
26031
+ if (!err || typeof err !== 'object')
26032
+ return false;
26033
+ return err.code === 'UNPREDICTABLE_GAS_LIMIT';
26034
+ }
26035
+
26036
+ export { APXIUM, AU_ALPHA3, AU_MIN_CUMULATIVE_BY_STABLE, CHAIN_CONFIGS, CLEARING_WINDOW_SECONDS, DepositsFacade, EPOCHS_IN_YEAR, INVOICEMATE, Kasu, KasuSdk, MAX_LENDING_AMOUNT_FALLBACK, MIN_LENDING_AMOUNT_FALLBACK, MIN_TRANCHE_CAPACITY, NO_DIRECTUS_URL_MESSAGE, PortfolioFacade, RIXON_CAPITAL, SdkConfig, StrategiesFacade, UPPER_MEZZANINE, apyToEpochRate, asContractType, auMinimumRemaining, auThresholdFor, buildContractVersionType, buildFullNameRequestMessage, buildLegacyContractRequestMessage, buildLoanAgreementSignMessage, ceilToCents, compareTrancheSeniority, computeSettlementWindow, countSubmissions, deriveCycleDates, derivePoolStatus, deriveRequestState, encodeDepositData, epochRateToApy, fetchUnusedPoolIds, firstSubmissionTimestamp, floorToCents, formatSignTimestampUtc, getCreditOriginator, getInstitutionalLender, getTrancheDisplayName, isAuMinimumExempt, isAustralianKyc, isBelowMinimumCapacity, isCycleClosed, isUnpredictableGas, isUserRejected, maxNetRateCeiling, netEffectiveApy, netTrancheApyBounds, nextCycleBoundary, parseFormattedMessage, parseMinorUnits, parseTrancheBound, pickDefaultTrancheId, pickHighestYieldTranche, poolAllTranchesFull, poolMaxApy, resolveBoundShortcuts, resolveDepositBounds, selectVisiblePools, submissionEvents, trancheApyBounds, trancheHasCapacity, trancheRiskRank };