@kasufinance/kasu-sdk 2.14.0 → 2.15.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 +29 -5
- package/dist/bundle.cjs.js +561 -11
- package/dist/bundle.esm.js +553 -12
- package/dist/domain/index.d.ts +6 -1
- package/dist/domain/index.js +6 -2
- package/dist/domain/index.js.map +1 -1
- package/dist/domain/loan-contract.d.ts +16 -5
- package/dist/domain/loan-contract.js +16 -5
- package/dist/domain/loan-contract.js.map +1 -1
- package/dist/domain/pool-tvl.js +179 -0
- package/dist/domain/pool-tvl.js.map +1 -1
- package/dist/domain/requests.js +29 -19
- package/dist/domain/requests.js.map +1 -1
- package/dist/domain/strategy-details.d.ts +262 -0
- package/dist/domain/strategy-details.js +375 -0
- package/dist/domain/strategy-details.js.map +1 -0
- package/dist/facade/index.d.ts +1 -1
- package/dist/facade/index.js +2 -0
- package/dist/facade/index.js.map +1 -1
- package/dist/facade/strategies.d.ts +85 -3
- package/dist/facade/strategies.js +182 -6
- package/dist/facade/strategies.js.map +1 -1
- package/dist/facade/strategy-deployments.js +97 -0
- package/dist/facade/strategy-deployments.js.map +1 -0
- package/dist/facade/types.d.ts +82 -0
- package/dist/flows/deposit-flow.d.ts +19 -5
- package/dist/flows/deposit-flow.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/services/DataService/data-service.js +80 -9
- package/dist/services/DataService/data-service.js.map +1 -1
- package/dist/services/DataService/tvl-history.js +89 -0
- package/dist/services/DataService/tvl-history.js.map +1 -1
- package/dist/services/UserLending/queries.js +2 -7
- package/dist/services/UserLending/queries.js.map +1 -1
- package/dist/services/UserLending/subgraph-types.js.map +1 -1
- package/dist/services/UserLending/types.js.map +1 -1
- package/dist/services/UserLending/user-lending.js +72 -16
- package/dist/services/UserLending/user-lending.js.map +1 -1
- package/package.json +1 -1
- package/src/domain/index.ts +27 -1
- package/src/domain/loan-contract.ts +16 -5
- package/src/domain/strategy-details.ts +528 -0
- package/src/facade/index.ts +3 -0
- package/src/facade/strategies.ts +205 -10
- package/src/facade/types.ts +85 -0
- package/src/flows/deposit-flow.ts +19 -5
package/dist/bundle.cjs.js
CHANGED
|
@@ -27878,19 +27878,30 @@ function buildLoanAgreementSignMessage(p) {
|
|
|
27878
27878
|
].join('\n');
|
|
27879
27879
|
}
|
|
27880
27880
|
/**
|
|
27881
|
-
* The legacy `/contract/generate`
|
|
27881
|
+
* The legacy `/contract/generate` message.
|
|
27882
27882
|
*
|
|
27883
27883
|
* ⚠️ BYTE-EXACT PROTOCOL STRING. kasu-backend rebuilds it as
|
|
27884
27884
|
* `` `I request contract content for ${address} at ${timestamp}.` `` from the
|
|
27885
27885
|
* `address` and `timestamp` fields of the request body — so the string signed
|
|
27886
27886
|
* and the body sent must agree exactly, INCLUDING the address casing. This
|
|
27887
27887
|
* builder lowercases, and the request body must carry the same lowercased
|
|
27888
|
-
* address
|
|
27888
|
+
* address.
|
|
27889
27889
|
*
|
|
27890
27890
|
* The backend takes this path whenever the four human-readable display fields
|
|
27891
|
-
* are absent
|
|
27892
|
-
*
|
|
27893
|
-
* signs
|
|
27891
|
+
* are absent. Only the retired kasu-fe-next app (retired 2026-11-05) and
|
|
27892
|
+
* kasu-mobile builds from before 2026-09-06 signed it; every current consumer
|
|
27893
|
+
* signs `buildLoanAgreementSignMessage`. The backend keeps accepting it until
|
|
27894
|
+
* no such mobile build is left in use.
|
|
27895
|
+
*
|
|
27896
|
+
* It is NOT the `/contract/resolve` message. Resolve has a message of its own
|
|
27897
|
+
* (`Requesting contract content for {address}/{id} at {timestamp}.`), which
|
|
27898
|
+
* kasu-backend rebuilds and verifies, so it does not depend on this format.
|
|
27899
|
+
*
|
|
27900
|
+
* @deprecated Sign the human-readable loan-agreement message
|
|
27901
|
+
* (`buildLoanAgreementSignMessage`, `contractMessage.format:
|
|
27902
|
+
* 'loan-agreement'`) instead. Only the retired kasu-fe-next app and
|
|
27903
|
+
* pre-2026-09-06 kasu-mobile builds used this format; it will be removed in
|
|
27904
|
+
* the next major version.
|
|
27894
27905
|
*
|
|
27895
27906
|
* @param timestampMs ms-epoch, and the same value sent as the body's
|
|
27896
27907
|
* `timestamp`.
|
|
@@ -29309,6 +29320,381 @@ function maxNetRateCeiling(pools, feePercent) {
|
|
|
29309
29320
|
return rate;
|
|
29310
29321
|
}
|
|
29311
29322
|
|
|
29323
|
+
/**
|
|
29324
|
+
* The CMS-authored figures and text a strategy's Details screen shows, read
|
|
29325
|
+
* the way kasu-ui's Details tab reads them, so every app shows the same
|
|
29326
|
+
* thing.
|
|
29327
|
+
*
|
|
29328
|
+
* Lifted from kasu-ui (`lib/details-fields.ts`, `lib/directus-text.ts` and
|
|
29329
|
+
* the parsers inside `deposit-dialog/details-tab.tsx`). The rules are facts
|
|
29330
|
+
* about how the CMS is authored, not presentation:
|
|
29331
|
+
*
|
|
29332
|
+
* - EMPTY IS UNAUTHORED. The CMS stores an emptied field as `''`, and its
|
|
29333
|
+
* "numeric" fields arrive as a number, a numeric string, `''` or `null`.
|
|
29334
|
+
* Anything that is not real data reads as `undefined` — never a `0`, a
|
|
29335
|
+
* placeholder or a sentence of the SDK's own. A field the caller cannot
|
|
29336
|
+
* read is a row the caller omits.
|
|
29337
|
+
* - A START DATE MUST DATE. A value that is not an ISO date, or a real
|
|
29338
|
+
* calendar date, or is before 2000 or in the future, is refused: rendered
|
|
29339
|
+
* as elapsed time it would claim a history nobody authored.
|
|
29340
|
+
* - SOME FIELDS CARRY TWO LINES IN ONE STRING. Business Types and Facility
|
|
29341
|
+
* Type are a primary value and a qualifying clause; risk-item
|
|
29342
|
+
* descriptions can be numbered enumerations. Splitting them is the same
|
|
29343
|
+
* everywhere, so it lives here.
|
|
29344
|
+
*
|
|
29345
|
+
* Like `pickShortDescription`, this authors no string of its own: it selects,
|
|
29346
|
+
* splits and trims text the CMS already holds. None of it is sanitised —
|
|
29347
|
+
* render every string as text, never as HTML or markdown.
|
|
29348
|
+
*
|
|
29349
|
+
* Pure. No clock: the date reader takes `now` from its caller.
|
|
29350
|
+
*/
|
|
29351
|
+
// ---------------------------------------------------------------------------
|
|
29352
|
+
// Start dates
|
|
29353
|
+
// ---------------------------------------------------------------------------
|
|
29354
|
+
/**
|
|
29355
|
+
* `YYYY-MM-DD`, optionally followed by a time part — the two shapes a CMS
|
|
29356
|
+
* date column serialises to. Checked BEFORE any parse: `new Date('Jun 2024')`
|
|
29357
|
+
* is a valid date in V8, so an editorial string would otherwise become a
|
|
29358
|
+
* confident duration.
|
|
29359
|
+
*/
|
|
29360
|
+
const ISO_DATE = /^(\d{4})-(\d{2})-(\d{2})(?:[T ].*)?$/;
|
|
29361
|
+
/** Nothing on Kasu has a lending history from before this year. */
|
|
29362
|
+
const EARLIEST_YEAR = 2000;
|
|
29363
|
+
/**
|
|
29364
|
+
* A CMS start date as its `YYYY-MM-DD` date head, or `undefined`.
|
|
29365
|
+
*
|
|
29366
|
+
* Accepts an ISO date, optionally with a time part (only the date head is
|
|
29367
|
+
* kept — a time changes nothing at year/month granularity, and converting it
|
|
29368
|
+
* through a timezone could move it a day). Refused, as `undefined`:
|
|
29369
|
+
*
|
|
29370
|
+
* - anything that is not a string (or, with `unixSeconds`, a number);
|
|
29371
|
+
* - an empty or blank string (unauthored);
|
|
29372
|
+
* - a string that is not ISO-shaped (`'Jun 2024'`);
|
|
29373
|
+
* - a date that does not exist (`'2024-02-30'`, `'2024-13-01'`) — never
|
|
29374
|
+
* rolled over into the next month;
|
|
29375
|
+
* - a year before 2000;
|
|
29376
|
+
* - a date after `now` (a typo such as 2062 for 2026 — no history has
|
|
29377
|
+
* begun in the future).
|
|
29378
|
+
*
|
|
29379
|
+
* `now` is the caller's clock; the facade passes the current time.
|
|
29380
|
+
*/
|
|
29381
|
+
function readCmsStartDate(value, now, options = {}) {
|
|
29382
|
+
let head;
|
|
29383
|
+
if (typeof value === 'number' && options.unixSeconds) {
|
|
29384
|
+
// `0` is formatDuration's "unset" sentinel; negatives are pre-1970.
|
|
29385
|
+
if (!Number.isFinite(value) || value <= 0)
|
|
29386
|
+
return undefined;
|
|
29387
|
+
const date = new Date(value * 1000);
|
|
29388
|
+
if (Number.isNaN(date.getTime()))
|
|
29389
|
+
return undefined;
|
|
29390
|
+
head = date.toISOString().slice(0, 10);
|
|
29391
|
+
}
|
|
29392
|
+
else if (typeof value === 'string') {
|
|
29393
|
+
const trimmed = value.trim();
|
|
29394
|
+
if (!ISO_DATE.test(trimmed))
|
|
29395
|
+
return undefined;
|
|
29396
|
+
head = trimmed.slice(0, 10);
|
|
29397
|
+
}
|
|
29398
|
+
else {
|
|
29399
|
+
return undefined;
|
|
29400
|
+
}
|
|
29401
|
+
const match = ISO_DATE.exec(head);
|
|
29402
|
+
if (!match)
|
|
29403
|
+
return undefined;
|
|
29404
|
+
const year = Number(match[1]);
|
|
29405
|
+
const month = Number(match[2]);
|
|
29406
|
+
const day = Number(match[3]);
|
|
29407
|
+
// Round-trip through UTC: a day or month out of range comes back as a
|
|
29408
|
+
// different date, which is how a non-existent date is caught.
|
|
29409
|
+
const utc = new Date(Date.UTC(year, month - 1, day));
|
|
29410
|
+
if (utc.getUTCFullYear() !== year ||
|
|
29411
|
+
utc.getUTCMonth() !== month - 1 ||
|
|
29412
|
+
utc.getUTCDate() !== day) {
|
|
29413
|
+
return undefined;
|
|
29414
|
+
}
|
|
29415
|
+
if (year < EARLIEST_YEAR)
|
|
29416
|
+
return undefined;
|
|
29417
|
+
if (utc.getTime() > now.getTime())
|
|
29418
|
+
return undefined;
|
|
29419
|
+
return head;
|
|
29420
|
+
}
|
|
29421
|
+
/**
|
|
29422
|
+
* Business Types (the CMS `industryExposure`), split into the value and its
|
|
29423
|
+
* supporting clause on the FIRST newline, else the FIRST parenthesised
|
|
29424
|
+
* segment (a trailing `)` dropped):
|
|
29425
|
+
*
|
|
29426
|
+
* `'SMEs & Large Businesses (diversified industries – exclusions apply)'`
|
|
29427
|
+
* → `{ primary: 'SMEs & Large Businesses', detail: 'diversified industries – exclusions apply' }`
|
|
29428
|
+
*
|
|
29429
|
+
* A value with neither delimiter is `{ primary }` alone. `undefined` for a
|
|
29430
|
+
* missing, non-string, empty or blank value.
|
|
29431
|
+
*
|
|
29432
|
+
* REFINED over kasu-ui: a value that OPENS with `(` would split to an empty
|
|
29433
|
+
* primary there; here the whole value is the primary instead, so `primary`
|
|
29434
|
+
* is never empty. No live record has that shape.
|
|
29435
|
+
*/
|
|
29436
|
+
function splitBusinessTypes(raw) {
|
|
29437
|
+
const value = typeof raw === 'string' ? raw.trim() : '';
|
|
29438
|
+
if (!value)
|
|
29439
|
+
return undefined;
|
|
29440
|
+
let primary = value;
|
|
29441
|
+
let detail = '';
|
|
29442
|
+
const newlineAt = value.indexOf('\n');
|
|
29443
|
+
if (newlineAt !== -1) {
|
|
29444
|
+
primary = value.slice(0, newlineAt).trim();
|
|
29445
|
+
detail = value.slice(newlineAt + 1).trim();
|
|
29446
|
+
}
|
|
29447
|
+
else {
|
|
29448
|
+
const parenAt = value.indexOf('(');
|
|
29449
|
+
if (parenAt !== -1) {
|
|
29450
|
+
primary = value.slice(0, parenAt).trim();
|
|
29451
|
+
detail = value.slice(parenAt + 1).trim();
|
|
29452
|
+
if (detail.endsWith(')'))
|
|
29453
|
+
detail = detail.slice(0, -1).trim();
|
|
29454
|
+
}
|
|
29455
|
+
}
|
|
29456
|
+
if (!primary)
|
|
29457
|
+
return { primary: value };
|
|
29458
|
+
return detail ? { primary, detail } : { primary };
|
|
29459
|
+
}
|
|
29460
|
+
/**
|
|
29461
|
+
* The start of the label, or a character that is neither a word character
|
|
29462
|
+
* nor a hyphen, before `revolv` — not `\b`, which treats a hyphen as a
|
|
29463
|
+
* boundary, so "Non-Revolving" would match. kasu-ui writes the same rule as
|
|
29464
|
+
* the lookbehind `(?<![\w-])`; this form needs no lookbehind support from the
|
|
29465
|
+
* JavaScript engine.
|
|
29466
|
+
*/
|
|
29467
|
+
const REVOLVING_FACILITY = /(?:^|[^\w-])revolv\w*\b/i;
|
|
29468
|
+
/**
|
|
29469
|
+
* Facility Type (the CMS `loanStructure`): the label on the FIRST line, and
|
|
29470
|
+
* everything after the first newline as its clause, so a clause with line
|
|
29471
|
+
* breaks of its own stays whole.
|
|
29472
|
+
*
|
|
29473
|
+
* `'Revolving Instalment Facility\n12-24 months instalment loan, subject to annual review'`
|
|
29474
|
+
* → `{ label: 'Revolving Instalment Facility', detail: '12-24 months …', revolving: true }`
|
|
29475
|
+
*
|
|
29476
|
+
* `undefined` for a missing, non-string, empty or blank value. The value is
|
|
29477
|
+
* trimmed first, so a leading blank line still yields a label.
|
|
29478
|
+
*/
|
|
29479
|
+
function parseFacility(raw) {
|
|
29480
|
+
const value = typeof raw === 'string' ? raw.trim() : '';
|
|
29481
|
+
if (!value)
|
|
29482
|
+
return undefined;
|
|
29483
|
+
const breakAt = value.indexOf('\n');
|
|
29484
|
+
const label = (breakAt === -1 ? value : value.slice(0, breakAt)).trim();
|
|
29485
|
+
const detail = breakAt === -1 ? '' : value.slice(breakAt + 1).trim();
|
|
29486
|
+
const revolving = REVOLVING_FACILITY.test(label);
|
|
29487
|
+
return detail ? { label, detail, revolving } : { label, revolving };
|
|
29488
|
+
}
|
|
29489
|
+
// ---------------------------------------------------------------------------
|
|
29490
|
+
// Risk Management
|
|
29491
|
+
// ---------------------------------------------------------------------------
|
|
29492
|
+
/**
|
|
29493
|
+
* The order the Risk Management groups read in, by group name (compared
|
|
29494
|
+
* trimmed, case-insensitively). The CMS returns items in authoring order,
|
|
29495
|
+
* which is not this. A group not listed still appears — after these, in CMS
|
|
29496
|
+
* order.
|
|
29497
|
+
*/
|
|
29498
|
+
const RISK_GROUP_ORDER = Object.freeze([
|
|
29499
|
+
'Security & Recourse',
|
|
29500
|
+
'Structural Controls',
|
|
29501
|
+
'Financial Covenants',
|
|
29502
|
+
'Reporting Covenants',
|
|
29503
|
+
'Key Eligibility Criteria',
|
|
29504
|
+
]);
|
|
29505
|
+
/** Zero-width paste artifacts, which `\s` does not match. */
|
|
29506
|
+
const ZERO_WIDTH = /[-]/g;
|
|
29507
|
+
/** An `N)` marker at the start, or after a `;` / `; and,` separator. */
|
|
29508
|
+
const HAS_ENUMERATION = /(?:^\d+\)|;\s*(?:and,?\s*)?\d+\))/;
|
|
29509
|
+
const ENUMERATION_SPLIT = /(?:^|\s*;\s*(?:and,?\s*)?)\d+\)\s*/;
|
|
29510
|
+
/** kasu-ui's stub for an unauthored description; never a clause. */
|
|
29511
|
+
const EMPTY_DESCRIPTION = '—';
|
|
29512
|
+
/**
|
|
29513
|
+
* A risk-item description as the lines beneath the item's name.
|
|
29514
|
+
*
|
|
29515
|
+
* A numbered enumeration (`'1) Foo; and, 2) Bar'`, or one that omits its
|
|
29516
|
+
* leading `1)`: `'Foo; 2) Bar'`) is one line per clause; anything else is
|
|
29517
|
+
* one line. Zero-width characters are stripped first. An unauthored
|
|
29518
|
+
* description is NO lines (`[]`), never a dash.
|
|
29519
|
+
*/
|
|
29520
|
+
function splitNumberedClauses(description) {
|
|
29521
|
+
const raw = cleanDescription(description);
|
|
29522
|
+
if (!raw)
|
|
29523
|
+
return [];
|
|
29524
|
+
const clauses = HAS_ENUMERATION.test(raw)
|
|
29525
|
+
? raw
|
|
29526
|
+
.split(ENUMERATION_SPLIT)
|
|
29527
|
+
.map((part) => part.trim())
|
|
29528
|
+
.filter((part) => part.length > 0)
|
|
29529
|
+
: [raw];
|
|
29530
|
+
const lines = clauses.length > 0 ? clauses : [raw];
|
|
29531
|
+
return lines.filter((line) => line !== EMPTY_DESCRIPTION);
|
|
29532
|
+
}
|
|
29533
|
+
/** A description with zero-width characters stripped, trimmed; `''` when not a string. */
|
|
29534
|
+
function cleanDescription(description) {
|
|
29535
|
+
return typeof description === 'string'
|
|
29536
|
+
? description.replace(ZERO_WIDTH, '').trim()
|
|
29537
|
+
: '';
|
|
29538
|
+
}
|
|
29539
|
+
/**
|
|
29540
|
+
* Risk-management items grouped by their group, the groups in
|
|
29541
|
+
* `RISK_GROUP_ORDER` and, within a group, the items in CMS order.
|
|
29542
|
+
*
|
|
29543
|
+
* An item with no name, or no group, is left out: there is nothing to head
|
|
29544
|
+
* its lines with, or no group to place it in. (The CMS joins items to groups
|
|
29545
|
+
* by name, so neither occurs in live data.) No group is ever empty, so an
|
|
29546
|
+
* item list with nothing usable gives `[]`.
|
|
29547
|
+
*/
|
|
29548
|
+
function groupRiskManagementItems(items) {
|
|
29549
|
+
const groups = new Map();
|
|
29550
|
+
for (const source of items) {
|
|
29551
|
+
const name = trimmedOrUndefined(source.name);
|
|
29552
|
+
const group = trimmedOrUndefined(source.group);
|
|
29553
|
+
if (!name || !group)
|
|
29554
|
+
continue;
|
|
29555
|
+
const clauses = splitNumberedClauses(source.description);
|
|
29556
|
+
// The same cleaned text the clauses come from, and only when they
|
|
29557
|
+
// say something: a description of "—" or of zero-width characters
|
|
29558
|
+
// alone is unauthored, so it is absent here too, never a stray dash.
|
|
29559
|
+
const description = clauses.length > 0 ? cleanDescription(source.description) : '';
|
|
29560
|
+
const tooltip = trimmedOrUndefined(source.tooltip);
|
|
29561
|
+
const item = Object.assign(Object.assign(Object.assign({ name }, (description ? { description } : {})), { clauses }), (tooltip ? { tooltip } : {}));
|
|
29562
|
+
const existing = groups.get(group);
|
|
29563
|
+
if (existing)
|
|
29564
|
+
existing.items.push(item);
|
|
29565
|
+
else
|
|
29566
|
+
groups.set(group, { group, items: [item] });
|
|
29567
|
+
}
|
|
29568
|
+
// Array.prototype.sort is stable, so unlisted groups keep CMS order.
|
|
29569
|
+
return [...groups.values()].sort((a, b) => riskGroupRank(a.group) - riskGroupRank(b.group));
|
|
29570
|
+
}
|
|
29571
|
+
const RISK_GROUP_RANK = new Map(RISK_GROUP_ORDER.map((group, index) => [group.toLowerCase(), index]));
|
|
29572
|
+
function riskGroupRank(group) {
|
|
29573
|
+
var _a;
|
|
29574
|
+
return ((_a = RISK_GROUP_RANK.get(group.trim().toLowerCase())) !== null && _a !== void 0 ? _a : RISK_GROUP_ORDER.length);
|
|
29575
|
+
}
|
|
29576
|
+
/**
|
|
29577
|
+
* The pool's security tags (the CMS `security` list), trimmed, blanks
|
|
29578
|
+
* dropped. `undefined` when none remain.
|
|
29579
|
+
*
|
|
29580
|
+
* kasu-ui shows these under "Security Structure over End Borrowers" ONLY
|
|
29581
|
+
* when the pool has no risk-management items.
|
|
29582
|
+
*/
|
|
29583
|
+
function securityStructureOf(raw) {
|
|
29584
|
+
if (!Array.isArray(raw))
|
|
29585
|
+
return undefined;
|
|
29586
|
+
const tags = raw
|
|
29587
|
+
.map((tag) => trimmedOrUndefined(tag))
|
|
29588
|
+
.filter((tag) => tag !== undefined);
|
|
29589
|
+
return tags.length > 0 ? tags : undefined;
|
|
29590
|
+
}
|
|
29591
|
+
// ---------------------------------------------------------------------------
|
|
29592
|
+
// Numbers
|
|
29593
|
+
// ---------------------------------------------------------------------------
|
|
29594
|
+
/**
|
|
29595
|
+
* A CMS or subgraph "numeric" value as a finite number, or `undefined`. An
|
|
29596
|
+
* empty or blank string is UNAUTHORED, not zero; a genuine `0` survives.
|
|
29597
|
+
*/
|
|
29598
|
+
function finiteOrUndefined(value) {
|
|
29599
|
+
if (typeof value === 'number') {
|
|
29600
|
+
return Number.isFinite(value) ? value : undefined;
|
|
29601
|
+
}
|
|
29602
|
+
if (typeof value === 'string') {
|
|
29603
|
+
if (value.trim() === '')
|
|
29604
|
+
return undefined;
|
|
29605
|
+
const numeric = Number(value);
|
|
29606
|
+
return Number.isFinite(numeric) ? numeric : undefined;
|
|
29607
|
+
}
|
|
29608
|
+
return undefined;
|
|
29609
|
+
}
|
|
29610
|
+
/**
|
|
29611
|
+
* A leg `strategyTotalsUsd` cannot place: two legs for one deployment
|
|
29612
|
+
* (`duplicate-leg`, which would double-count it), or a leg for a deployment
|
|
29613
|
+
* not in `expectedDeployments` (`unexpected-leg`, a pool of some other
|
|
29614
|
+
* strategy). Thrown, because both are caller bugs, not data states.
|
|
29615
|
+
*/
|
|
29616
|
+
class StrategyTotalsInputError extends Error {
|
|
29617
|
+
constructor(code, deployment) {
|
|
29618
|
+
super(code === 'duplicate-leg'
|
|
29619
|
+
? `strategyTotalsUsd: two legs for deployment "${deployment}"`
|
|
29620
|
+
: `strategyTotalsUsd: leg for deployment "${deployment}" is not in expectedDeployments`);
|
|
29621
|
+
this.code = code;
|
|
29622
|
+
this.deployment = deployment;
|
|
29623
|
+
this.name = 'StrategyTotalsInputError';
|
|
29624
|
+
}
|
|
29625
|
+
}
|
|
29626
|
+
/**
|
|
29627
|
+
* Total Active Lending and Lender Interest Earned for a strategy across the
|
|
29628
|
+
* deployments it lends on, in USD: each leg's native amount times that leg's
|
|
29629
|
+
* own `usdRate`, summed. Tax Pay lends on Base (USDC) and XDC (AUDD);
|
|
29630
|
+
* 2,567,901 AUDD is not US$2,567,901.
|
|
29631
|
+
*
|
|
29632
|
+
* `expectedDeployments` is every deployment the strategy has a pool on —
|
|
29633
|
+
* `strategyDeployments(poolId, chain).map((d) => d.chain)` — and `legs` the
|
|
29634
|
+
* ones whose pool the caller actually loaded. What is guaranteed:
|
|
29635
|
+
*
|
|
29636
|
+
* - **`null` — WITHHELD** when there are no legs, or ANY leg's rate is
|
|
29637
|
+
* missing or unusable (not a finite number above 0). The total is never
|
|
29638
|
+
* built on a guessed rate, and never silently drops a loaded leg; the
|
|
29639
|
+
* caller shows the held pool's own figure, or nothing.
|
|
29640
|
+
* - **Otherwise the sum over `legs`**, with `isPartial` /
|
|
29641
|
+
* `missingDeployments` stating exactly which expected deployments it
|
|
29642
|
+
* does NOT cover. `isPartial: false` is the only case in which the
|
|
29643
|
+
* figures are the whole strategy.
|
|
29644
|
+
* - An amount that is absent or malformed contributes nothing (a real `0`
|
|
29645
|
+
* is still `0`).
|
|
29646
|
+
* - Throws `StrategyTotalsInputError` for a duplicated leg or one not in
|
|
29647
|
+
* `expectedDeployments`, before summing anything.
|
|
29648
|
+
*
|
|
29649
|
+
* Only these two figures are summed. The CMS-authored ones — every
|
|
29650
|
+
* originator figure, `operatingSince`, the loss rates — are shared by a
|
|
29651
|
+
* strategy's pools and already strategy-wide: summing them double-counts.
|
|
29652
|
+
*
|
|
29653
|
+
* Mirrors the money half of kasu-ui's `aggregateStrategyPools` and its
|
|
29654
|
+
* withhold gate.
|
|
29655
|
+
*/
|
|
29656
|
+
function strategyTotalsUsd(legs, expectedDeployments) {
|
|
29657
|
+
var _a, _b;
|
|
29658
|
+
const expected = new Set(expectedDeployments);
|
|
29659
|
+
const seen = new Set();
|
|
29660
|
+
for (const leg of legs) {
|
|
29661
|
+
if (seen.has(leg.deployment)) {
|
|
29662
|
+
throw new StrategyTotalsInputError('duplicate-leg', leg.deployment);
|
|
29663
|
+
}
|
|
29664
|
+
if (!expected.has(leg.deployment)) {
|
|
29665
|
+
throw new StrategyTotalsInputError('unexpected-leg', leg.deployment);
|
|
29666
|
+
}
|
|
29667
|
+
seen.add(leg.deployment);
|
|
29668
|
+
}
|
|
29669
|
+
if (legs.length === 0)
|
|
29670
|
+
return null;
|
|
29671
|
+
let totalActiveLendingUsd = 0;
|
|
29672
|
+
let lenderInterestEarnedUsd = 0;
|
|
29673
|
+
for (const leg of legs) {
|
|
29674
|
+
const rate = leg.usdRate;
|
|
29675
|
+
if (typeof rate !== 'number' || !Number.isFinite(rate) || rate <= 0) {
|
|
29676
|
+
return null;
|
|
29677
|
+
}
|
|
29678
|
+
totalActiveLendingUsd +=
|
|
29679
|
+
((_a = finiteOrUndefined(leg.totalActiveLending)) !== null && _a !== void 0 ? _a : 0) * rate;
|
|
29680
|
+
lenderInterestEarnedUsd +=
|
|
29681
|
+
((_b = finiteOrUndefined(leg.lenderInterestEarned)) !== null && _b !== void 0 ? _b : 0) * rate;
|
|
29682
|
+
}
|
|
29683
|
+
const missingDeployments = [...expected].filter((d) => !seen.has(d));
|
|
29684
|
+
return {
|
|
29685
|
+
totalActiveLendingUsd,
|
|
29686
|
+
lenderInterestEarnedUsd,
|
|
29687
|
+
isPartial: missingDeployments.length > 0,
|
|
29688
|
+
missingDeployments,
|
|
29689
|
+
};
|
|
29690
|
+
}
|
|
29691
|
+
function trimmedOrUndefined(value) {
|
|
29692
|
+
if (typeof value !== 'string')
|
|
29693
|
+
return undefined;
|
|
29694
|
+
const trimmed = value.trim();
|
|
29695
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
29696
|
+
}
|
|
29697
|
+
|
|
29312
29698
|
/**
|
|
29313
29699
|
* A strategy's SHORT blurb, as plain text: the CMS `liteDescription` first,
|
|
29314
29700
|
* else the first paragraph of the long `description`, else nothing.
|
|
@@ -29396,10 +29782,55 @@ class StrategiesFacade {
|
|
|
29396
29782
|
* Chain pool id → the pool id its CMS record is keyed by (XDC pools
|
|
29397
29783
|
* share their Base counterpart's editorial record). Lowercase keys.
|
|
29398
29784
|
*/
|
|
29399
|
-
_poolMetadataMapping = {}
|
|
29785
|
+
_poolMetadataMapping = {},
|
|
29786
|
+
/**
|
|
29787
|
+
* The clock the CMS start dates are checked against (a date after it
|
|
29788
|
+
* is refused). Injected for tests; the current time otherwise.
|
|
29789
|
+
*/
|
|
29790
|
+
_now = () => new Date(),
|
|
29791
|
+
/**
|
|
29792
|
+
* Milliseconds on a clock that only moves forward, for the shared
|
|
29793
|
+
* CMS reads' 30-second lifetime — separate from `_now`, so a pinned
|
|
29794
|
+
* date clock cannot keep a read alive for ever. Injected for tests;
|
|
29795
|
+
* `Date.now` otherwise.
|
|
29796
|
+
*/
|
|
29797
|
+
_memoClockMs = () => Date.now()) {
|
|
29400
29798
|
this._dataService = _dataService;
|
|
29401
29799
|
this._userLending = _userLending;
|
|
29402
29800
|
this._poolMetadataMapping = _poolMetadataMapping;
|
|
29801
|
+
this._now = _now;
|
|
29802
|
+
this._memoClockMs = _memoClockMs;
|
|
29803
|
+
/**
|
|
29804
|
+
* Short-lived, per-instance memo of the two CMS-wide reads the pool
|
|
29805
|
+
* getters share, so a Details screen asking for the loss rate, the risk
|
|
29806
|
+
* groups and the originator together reads each collection once. Keyed
|
|
29807
|
+
* by read; holds the in-flight promise, so concurrent callers share it.
|
|
29808
|
+
* A REJECTED read is dropped at once, never served again — the next call
|
|
29809
|
+
* retries.
|
|
29810
|
+
*/
|
|
29811
|
+
this._cmsReads = new Map();
|
|
29812
|
+
}
|
|
29813
|
+
_memoCmsRead(key, read) {
|
|
29814
|
+
const nowMs = this._memoClockMs();
|
|
29815
|
+
const cached = this._cmsReads.get(key);
|
|
29816
|
+
if (cached && cached.expiresAt > nowMs) {
|
|
29817
|
+
return cached.promise;
|
|
29818
|
+
}
|
|
29819
|
+
const promise = read();
|
|
29820
|
+
const entry = { promise, expiresAt: nowMs + CMS_READ_TTL_MS };
|
|
29821
|
+
this._cmsReads.set(key, entry);
|
|
29822
|
+
promise.catch(() => {
|
|
29823
|
+
// Only our own entry: a newer read may have replaced it.
|
|
29824
|
+
if (this._cmsReads.get(key) === entry)
|
|
29825
|
+
this._cmsReads.delete(key);
|
|
29826
|
+
});
|
|
29827
|
+
return promise;
|
|
29828
|
+
}
|
|
29829
|
+
_riskManagementRecords() {
|
|
29830
|
+
return this._memoCmsRead('riskManagement', () => this._dataService.getRiskManagement());
|
|
29831
|
+
}
|
|
29832
|
+
_originatorProfiles() {
|
|
29833
|
+
return this._memoCmsRead('originatorProfiles', () => this._dataService.getPoolDelegateProfileAndHistory());
|
|
29403
29834
|
}
|
|
29404
29835
|
/**
|
|
29405
29836
|
* The pool's loss rate and first-loss capital, from its CMS
|
|
@@ -29412,12 +29843,14 @@ class StrategiesFacade {
|
|
|
29412
29843
|
* 0..1 fraction.
|
|
29413
29844
|
*
|
|
29414
29845
|
* Requires `directusUrl`; without it the call rejects with
|
|
29415
|
-
* `NO_DIRECTUS_URL_MESSAGE`, like every CMS-only read.
|
|
29846
|
+
* `NO_DIRECTUS_URL_MESSAGE`, like every CMS-only read. Shares its CMS
|
|
29847
|
+
* read with `getRiskManagementGroups` for 30 seconds per instance; a
|
|
29848
|
+
* failed read is never reused.
|
|
29416
29849
|
*/
|
|
29417
29850
|
getRiskPerformance(poolId) {
|
|
29418
29851
|
return __awaiter(this, void 0, void 0, function* () {
|
|
29419
29852
|
const metadataId = this._metadataIdOf(poolId);
|
|
29420
|
-
const records = yield this.
|
|
29853
|
+
const records = yield this._riskManagementRecords();
|
|
29421
29854
|
const record = records.find((r) => String(r.poolIdFK).toLowerCase() === metadataId);
|
|
29422
29855
|
if (!record)
|
|
29423
29856
|
return null;
|
|
@@ -29441,16 +29874,95 @@ class StrategiesFacade {
|
|
|
29441
29874
|
* deployment every profile lists no pools and this resolves `null` —
|
|
29442
29875
|
* the same answer kasu-ui's own lookup gives there today. The metadata
|
|
29443
29876
|
* candidate becomes live once the service resolves the mapping.
|
|
29877
|
+
*
|
|
29878
|
+
* Shares its reads with `getOriginatorHistory` for 30 seconds per
|
|
29879
|
+
* instance; a failed read is never reused. Each call returns its own
|
|
29880
|
+
* copy of the profile, so mutating it changes nothing shared.
|
|
29444
29881
|
*/
|
|
29445
29882
|
getOriginator(poolId) {
|
|
29446
29883
|
return __awaiter(this, void 0, void 0, function* () {
|
|
29447
|
-
var _a;
|
|
29448
29884
|
const candidates = new Set([
|
|
29449
29885
|
poolId.toLowerCase(),
|
|
29450
29886
|
this._metadataIdOf(poolId),
|
|
29451
29887
|
]);
|
|
29452
|
-
const profiles = yield this.
|
|
29453
|
-
|
|
29888
|
+
const profiles = yield this._originatorProfiles();
|
|
29889
|
+
const profile = profiles.find((candidate) => candidate.otherKASUPools.some((pool) => candidates.has(String(pool.id).toLowerCase())));
|
|
29890
|
+
// A copy: the profiles are shared between calls (see
|
|
29891
|
+
// `_memoCmsRead`), so a caller sorting or editing its result must
|
|
29892
|
+
// not change what the next call returns.
|
|
29893
|
+
return profile
|
|
29894
|
+
? Object.assign(Object.assign({}, profile), { otherKASUPools: profile.otherKASUPools.map((pool) => (Object.assign({}, pool))) }) : null;
|
|
29895
|
+
});
|
|
29896
|
+
}
|
|
29897
|
+
/**
|
|
29898
|
+
* The credit originator's ENTIRE lending history for a pool — since when,
|
|
29899
|
+
* total originated, loans outstanding, loss rate — read from the profile
|
|
29900
|
+
* `getOriginator` finds, each figure validated. `null` when no profile
|
|
29901
|
+
* lists the pool, or the profile authors none of the four.
|
|
29902
|
+
*
|
|
29903
|
+
* kasu-ui's Details tab shows these as its Credit Originator strip, and
|
|
29904
|
+
* hides the strip when there is no profile. The money figures are
|
|
29905
|
+
* editorial USD on every deployment: never FX-convert them. An XDC pool
|
|
29906
|
+
* resolves `null` today, as it does in kasu-ui (see `getOriginator`).
|
|
29907
|
+
*
|
|
29908
|
+
* Requires `directusUrl`, like every CMS-only read; rejects when the
|
|
29909
|
+
* read fails. Shares its reads with `getOriginator` for 30 seconds per
|
|
29910
|
+
* instance; a failed read is never reused.
|
|
29911
|
+
*/
|
|
29912
|
+
getOriginatorHistory(poolId) {
|
|
29913
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
29914
|
+
const profile = yield this.getOriginator(poolId);
|
|
29915
|
+
if (!profile)
|
|
29916
|
+
return null;
|
|
29917
|
+
const history = {};
|
|
29918
|
+
const since = readCmsStartDate(profile.delegateLendingHistory, this._now(), { unixSeconds: true });
|
|
29919
|
+
if (since !== undefined)
|
|
29920
|
+
history.since = since;
|
|
29921
|
+
const totalOriginated = finiteOrUndefined(profile.totalLoanFundsOriginated);
|
|
29922
|
+
if (totalOriginated !== undefined) {
|
|
29923
|
+
history.totalOriginated = totalOriginated;
|
|
29924
|
+
}
|
|
29925
|
+
const loansOutstanding = finiteOrUndefined(profile.loansUnderManagement);
|
|
29926
|
+
if (loansOutstanding !== undefined) {
|
|
29927
|
+
history.loansOutstanding = loansOutstanding;
|
|
29928
|
+
}
|
|
29929
|
+
const lossRate = finiteOrUndefined(profile.historicLossRate);
|
|
29930
|
+
if (lossRate !== undefined)
|
|
29931
|
+
history.lossRate = lossRate;
|
|
29932
|
+
return Object.keys(history).length > 0 ? history : null;
|
|
29933
|
+
});
|
|
29934
|
+
}
|
|
29935
|
+
/**
|
|
29936
|
+
* The pool's Risk Management groups, from its CMS risk-management record:
|
|
29937
|
+
* each group with its items (name, the pool's description of it split
|
|
29938
|
+
* into lines, the shared tooltip), the groups in `RISK_GROUP_ORDER`
|
|
29939
|
+
* (Security & Recourse, Structural Controls, Financial Covenants,
|
|
29940
|
+
* Reporting Covenants, Key Eligibility Criteria, then any other in CMS
|
|
29941
|
+
* order). See `groupRiskManagementItems`.
|
|
29942
|
+
*
|
|
29943
|
+
* `null` when the pool has no record, or the record has no usable item —
|
|
29944
|
+
* then kasu-ui falls back to `Strategy.securityStructure`. XDC pools
|
|
29945
|
+
* resolve through the metadata mapping to their Base record, like
|
|
29946
|
+
* `getRiskPerformance`.
|
|
29947
|
+
*
|
|
29948
|
+
* REJECTS when the CMS read fails (an outage, or no `directusUrl` —
|
|
29949
|
+
* `NO_DIRECTUS_URL_MESSAGE`), so a failure stays visible rather than
|
|
29950
|
+
* reading as "no items". For what to SHOW, treat a rejection like `null`:
|
|
29951
|
+
* fall back to `Strategy.securityStructure`, which arrives with the
|
|
29952
|
+
* strategy and needs no second read.
|
|
29953
|
+
*
|
|
29954
|
+
* Shares its CMS read with `getRiskPerformance` for 30 seconds per
|
|
29955
|
+
* instance; a failed read is never reused.
|
|
29956
|
+
*/
|
|
29957
|
+
getRiskManagementGroups(poolId) {
|
|
29958
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
29959
|
+
const metadataId = this._metadataIdOf(poolId);
|
|
29960
|
+
const records = yield this._riskManagementRecords();
|
|
29961
|
+
const record = records.find((r) => String(r.poolIdFK).toLowerCase() === metadataId);
|
|
29962
|
+
if (!record || !Array.isArray(record.items))
|
|
29963
|
+
return null;
|
|
29964
|
+
const groups = groupRiskManagementItems(record.items);
|
|
29965
|
+
return groups.length > 0 ? groups : null;
|
|
29454
29966
|
});
|
|
29455
29967
|
}
|
|
29456
29968
|
_metadataIdOf(poolId) {
|
|
@@ -29624,10 +30136,39 @@ class StrategiesFacade {
|
|
|
29624
30136
|
apyStructure: pool.poolApyStructure,
|
|
29625
30137
|
thumbnailUrl: pool.thumbnailImageUrl,
|
|
29626
30138
|
bannerUrl: pool.bannerImageUrl,
|
|
30139
|
+
strategyHistory: strategyHistoryOf(pool, this._now()),
|
|
30140
|
+
endBorrowers: endBorrowersOf(pool),
|
|
30141
|
+
securityStructure: securityStructureOf(pool.security),
|
|
29627
30142
|
_raw: pool,
|
|
29628
30143
|
};
|
|
29629
30144
|
}
|
|
29630
30145
|
}
|
|
30146
|
+
/**
|
|
30147
|
+
* How long a CMS-wide read is shared between the pool getters. Long enough
|
|
30148
|
+
* for one screen's worth of calls, short enough that a CMS edit shows on the
|
|
30149
|
+
* next visit; apps cache further on their own terms.
|
|
30150
|
+
*/
|
|
30151
|
+
const CMS_READ_TTL_MS = 30000;
|
|
30152
|
+
/** `Strategy.strategyHistory`: an absent figure is an absent key. */
|
|
30153
|
+
function strategyHistoryOf(pool, now) {
|
|
30154
|
+
const history = {};
|
|
30155
|
+
const since = readCmsStartDate(pool.operatingSince, now);
|
|
30156
|
+
if (since !== undefined)
|
|
30157
|
+
history.since = since;
|
|
30158
|
+
const lenderInterestEarned = finiteOrUndefined(pool.yieldEarned);
|
|
30159
|
+
if (lenderInterestEarned !== undefined) {
|
|
30160
|
+
history.lenderInterestEarned = lenderInterestEarned;
|
|
30161
|
+
}
|
|
30162
|
+
return history;
|
|
30163
|
+
}
|
|
30164
|
+
/** Business Types and Facility Type, or `undefined` when neither is authored. */
|
|
30165
|
+
function endBorrowersOf(pool) {
|
|
30166
|
+
const businessTypes = splitBusinessTypes(pool.industryExposure);
|
|
30167
|
+
const facility = parseFacility(pool.loanStructure);
|
|
30168
|
+
if (!businessTypes && !facility)
|
|
30169
|
+
return undefined;
|
|
30170
|
+
return Object.assign(Object.assign({}, (businessTypes ? { businessTypes } : {})), (facility ? { facility } : {}));
|
|
30171
|
+
}
|
|
29631
30172
|
/**
|
|
29632
30173
|
* A CMS text field as a string, or `undefined` when it is absent, not a
|
|
29633
30174
|
* string, or empty (blank included) — the CMS stores an emptied field as `''`, and an empty
|
|
@@ -31167,6 +31708,7 @@ exports.PoolTvlHistoryAbortedError = PoolTvlHistoryAbortedError;
|
|
|
31167
31708
|
exports.PoolTvlHistoryTimeoutError = PoolTvlHistoryTimeoutError;
|
|
31168
31709
|
exports.PortfolioFacade = PortfolioFacade;
|
|
31169
31710
|
exports.READ_ONLY_MESSAGE = READ_ONLY_MESSAGE;
|
|
31711
|
+
exports.RISK_GROUP_ORDER = RISK_GROUP_ORDER;
|
|
31170
31712
|
exports.RIXON_CAPITAL = RIXON_CAPITAL;
|
|
31171
31713
|
exports.SECONDS_PER_WEEK = SECONDS_PER_WEEK;
|
|
31172
31714
|
exports.SPONSORED_CALL_KINDS = SPONSORED_CALL_KINDS;
|
|
@@ -31175,6 +31717,7 @@ exports.SdkConfig = SdkConfig;
|
|
|
31175
31717
|
exports.SponsoredCallInputError = SponsoredCallInputError;
|
|
31176
31718
|
exports.StrategiesFacade = StrategiesFacade;
|
|
31177
31719
|
exports.StrategyDeploymentInputError = StrategyDeploymentInputError;
|
|
31720
|
+
exports.StrategyTotalsInputError = StrategyTotalsInputError;
|
|
31178
31721
|
exports.TvlHistoryInputError = TvlHistoryInputError;
|
|
31179
31722
|
exports.UPPER_MEZZANINE = UPPER_MEZZANINE;
|
|
31180
31723
|
exports.WithdrawFlow = WithdrawFlow;
|
|
@@ -31223,6 +31766,7 @@ exports.getCreditOriginator = getCreditOriginator;
|
|
|
31223
31766
|
exports.getInstitutionalLender = getInstitutionalLender;
|
|
31224
31767
|
exports.getStrategyRegion = getStrategyRegion;
|
|
31225
31768
|
exports.getTrancheDisplayName = getTrancheDisplayName;
|
|
31769
|
+
exports.groupRiskManagementItems = groupRiskManagementItems;
|
|
31226
31770
|
exports.isAuMinimumExempt = isAuMinimumExempt;
|
|
31227
31771
|
exports.isAustralianKyc = isAustralianKyc;
|
|
31228
31772
|
exports.isBelowMinimumCapacity = isBelowMinimumCapacity;
|
|
@@ -31238,6 +31782,7 @@ exports.maxNetRateCeiling = maxNetRateCeiling;
|
|
|
31238
31782
|
exports.netEffectiveApy = netEffectiveApy;
|
|
31239
31783
|
exports.netTrancheApyBounds = netTrancheApyBounds;
|
|
31240
31784
|
exports.nextCycleBoundary = nextCycleBoundary;
|
|
31785
|
+
exports.parseFacility = parseFacility;
|
|
31241
31786
|
exports.parseFormattedMessage = parseFormattedMessage;
|
|
31242
31787
|
exports.parseMinorUnits = parseMinorUnits;
|
|
31243
31788
|
exports.parseTrancheBound = parseTrancheBound;
|
|
@@ -31250,10 +31795,15 @@ exports.poolTvl = poolTvl;
|
|
|
31250
31795
|
exports.projectedAnnualEarnings = projectedAnnualEarnings;
|
|
31251
31796
|
exports.proveDepositInReceipt = proveDepositInReceipt;
|
|
31252
31797
|
exports.proveUserOperationInReceipt = proveUserOperationInReceipt;
|
|
31798
|
+
exports.readCmsStartDate = readCmsStartDate;
|
|
31253
31799
|
exports.resolveBoundShortcuts = resolveBoundShortcuts;
|
|
31254
31800
|
exports.resolveDepositBounds = resolveDepositBounds;
|
|
31801
|
+
exports.securityStructureOf = securityStructureOf;
|
|
31255
31802
|
exports.selectVisiblePools = selectVisiblePools;
|
|
31803
|
+
exports.splitBusinessTypes = splitBusinessTypes;
|
|
31804
|
+
exports.splitNumberedClauses = splitNumberedClauses;
|
|
31256
31805
|
exports.strategyDeployments = strategyDeployments;
|
|
31806
|
+
exports.strategyTotalsUsd = strategyTotalsUsd;
|
|
31257
31807
|
exports.submissionEvents = submissionEvents;
|
|
31258
31808
|
exports.submitBatch = submitBatch;
|
|
31259
31809
|
exports.submitSponsoredCall = submitSponsoredCall;
|