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