@kasufinance/kasu-sdk 2.10.2 → 2.11.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 CHANGED
@@ -149,6 +149,7 @@ application formats them in its own design system and language.
149
149
  | Yields | `weeklyInterest`, `dailyInterest`, `accruedInterest`, `projectedAnnualEarnings`, `weightedAverageApy`, `SECONDS_PER_WEEK` — NET of the performance fee |
150
150
  | Batch proof | `validateDepositBatchCalls`, `proveDepositInReceipt`, `DepositBatchMismatchError`, `ENTRY_POINT_V07` |
151
151
  | Tranche display name | `getTrancheDisplayName`, `UPPER_MEZZANINE` |
152
+ | Strategy summary | `pickShortDescription` — the CMS short blurb as one line of plain text, or `undefined` (also on every `Strategy` as `summary`) |
152
153
  | Requests | `deriveRequestState`, `submissionEvents`, `countSubmissions`, `firstSubmissionTimestamp`, `isCycleClosed` |
153
154
  | Settlement | `computeSettlementWindow`, `nextCycleBoundary`, `deriveCycleDates`, `CLEARING_WINDOW_SECONDS` |
154
155
  | Loan contract | `encodeDepositData`, `buildContractVersionType`, `buildLoanAgreementSignMessage`, `buildLegacyContractRequestMessage`, `buildFullNameRequestMessage`, `formatSignTimestampUtc`, `parseFormattedMessage`, `asContractType` |
@@ -169,6 +170,14 @@ Three rules worth knowing:
169
170
  wrong number when an input is out of domain. Render that as "no rate", never
170
171
  as zero, and never substitute a fee of `0` for one you have not loaded — a
171
172
  fee-less rate overstates what a lender earns.
173
+ - **`Strategy.summary` is CMS editorial, cleaned, never authored.** It is
174
+ `liteDescription` when usable, else the first paragraph of `description`,
175
+ with inline markdown markers stripped and whitespace collapsed to one line.
176
+ The `"Not Available"` placeholder and a blank value count as missing, and an
177
+ absent blurb is `undefined`: what to show instead is the app's decision.
178
+ `liteDescription` and `description` stay the raw CMS values. It is NOT
179
+ sanitised: HTML tags and link syntax pass through unchanged, so render it
180
+ as text, never as HTML or markdown.
172
181
  - **The tranche rename is display-only.** `getTrancheDisplayName` maps the
173
182
  `Senior` tranche to "Upper Mezzanine" on Apxium strategies, because the true
174
183
  senior position is held by an institutional lender. Call it at the view
@@ -558,6 +567,14 @@ network problem cannot fail a pull request that did not touch the network.
558
567
  CI runs `build-tc`, `build`, `test:unit` and `rollup-build` on every pull
559
568
  request.
560
569
 
570
+ `npm ci` also installs a **pre-push hook** (`.githooks/pre-push`) that runs the
571
+ same steps locally before every `git push`: first a lockfile check
572
+ (`npm ci --dry-run`, which fails when `package.json` and `package-lock.json`
573
+ disagree), then `build-tc`, `build`, `test:unit` and `rollup-build`. It stops at
574
+ the first failing step and names it. It checks the working tree, not the pushed
575
+ commits, so commit first. `SKIP_PREPUSH=1 git push` skips it with a warning; CI
576
+ is still the gate.
577
+
561
578
  ## Versioning & publishing
562
579
 
563
580
  The package follows semantic versioning. The published runtime entry is the
@@ -24219,9 +24219,10 @@ class SdkConfig {
24219
24219
  }
24220
24220
 
24221
24221
  /**
24222
- * The single-call `LendingPoolManager` requests a sponsored relay may carry,
24223
- * encoded by the SDK so no application hand-builds calldata or hand-maps the
24224
- * relay's `kind` for them.
24222
+ * The single-call requests a sponsored relay may carry — the four
24223
+ * `LendingPoolManager` requests, plus the stable asset's own `transfer` (P2P
24224
+ * Send) — encoded by the SDK so no application hand-builds calldata or
24225
+ * hand-maps the relay's `kind` for them.
24225
24226
  *
24226
24227
  * Every function here is PURE: addresses and ids in, `{ kind, call }` out.
24227
24228
  * Reading the chain (the shares a withdrawal is for) is the facade's job —
@@ -24249,6 +24250,11 @@ const SPONSORED_CALL_KINDS = [
24249
24250
  'cancel_withdrawal',
24250
24251
  /** `LendingPoolManager.requestFixedTermDepositWithdrawal(pool, lockId)`. */
24251
24252
  'withdraw_at_expiry',
24253
+ /**
24254
+ * `stableAsset.transfer(to, amount)` — P2P Send, to the chain's stable
24255
+ * asset rather than the manager. See {@link encodeStableTransfer}.
24256
+ */
24257
+ 'transfer',
24252
24258
  ];
24253
24259
  /**
24254
24260
  * Widened to the plain `Interface`: the typechain one overloads
@@ -24256,7 +24262,9 @@ const SPONSORED_CALL_KINDS = [
24256
24262
  * `FUNCTION_OF`.
24257
24263
  */
24258
24264
  const LENDING_POOL_MANAGER$2 = ILendingPoolManagerAbi__factory.createInterface();
24259
- /** The `LendingPoolManager` function behind each kind. */
24265
+ /** The generated ERC-20 interface — the `transfer` kind's one function. */
24266
+ const ERC20$2 = IERC20MetadataAbi__factory.createInterface();
24267
+ /** The `LendingPoolManager` function behind each manager kind. */
24260
24268
  const FUNCTION_OF = Object.freeze({
24261
24269
  withdrawal: 'requestWithdrawal',
24262
24270
  cancel_deposit: 'cancelDepositRequest',
@@ -24265,14 +24273,16 @@ const FUNCTION_OF = Object.freeze({
24265
24273
  });
24266
24274
  /**
24267
24275
  * The 4-byte selector of each kind's function, DERIVED from the generated
24268
- * `ILendingPoolManager` ABI — never typed in by hand, so an ABI change moves
24269
- * it and the cross-check spec against the relay's own pins goes red.
24276
+ * ABIs (`ILendingPoolManager`, and `IERC20Metadata` for `transfer`) — never
24277
+ * typed in by hand, so an ABI change moves it and the cross-check spec
24278
+ * against the relay's own pins goes red.
24270
24279
  */
24271
24280
  const SPONSORED_CALL_SELECTORS = Object.freeze({
24272
24281
  withdrawal: LENDING_POOL_MANAGER$2.getSighash(FUNCTION_OF.withdrawal),
24273
24282
  cancel_deposit: LENDING_POOL_MANAGER$2.getSighash(FUNCTION_OF.cancel_deposit),
24274
24283
  cancel_withdrawal: LENDING_POOL_MANAGER$2.getSighash(FUNCTION_OF.cancel_withdrawal),
24275
24284
  withdraw_at_expiry: LENDING_POOL_MANAGER$2.getSighash(FUNCTION_OF.withdraw_at_expiry),
24285
+ transfer: ERC20$2.getSighash('transfer'),
24276
24286
  });
24277
24287
  /**
24278
24288
  * What an encoder throws for an input it will not encode. `code` names the
@@ -24371,6 +24381,53 @@ function encodeWithdrawAtExpiryCall(lendingPoolManager, poolId, fixedTermDeposit
24371
24381
  toUint256(fixedTermDepositId, 'lock-id'),
24372
24382
  ]);
24373
24383
  }
24384
+ /**
24385
+ * `stableAsset.transfer(to, amount)` — `transfer`: P2P Send of the chain's
24386
+ * stable asset from the sender's own account (`msg.sender`) to `to`.
24387
+ *
24388
+ * `amount` is in the token's BASE units (6 decimals for USDC), and must be
24389
+ * greater than zero. `to` must be a well-formed, non-zero address (a
24390
+ * mixed-case one with a valid EIP-55 checksum). That is
24391
+ * ALL this encoder judges, because it is all a pure function can know.
24392
+ *
24393
+ * The recipient POLICY is the relay's job and is deliberately not restated
24394
+ * here: it refuses the sender's own account, the stable asset, the
24395
+ * `LendingPoolManager`, the precompile range, any address with contract code
24396
+ * (read on chain at submission), a sender our identity store does not hold
24397
+ * as verified, and anything over the sender's daily count and amount caps.
24398
+ * A call this encoder accepts can therefore still be refused — the app maps
24399
+ * the relay's refusal codes to its own words.
24400
+ *
24401
+ * Pass `stableAsset` from the chain config (`CHAIN_CONFIGS[chain].stableAsset
24402
+ * .address`) — the relay compares `to` against its own configured token and
24403
+ * refuses any other contract.
24404
+ */
24405
+ function encodeStableTransfer(input) {
24406
+ assertAddress(input.stableAsset, 'stable-asset');
24407
+ assertAddress(input.to, 'recipient');
24408
+ // A mixed-case recipient carries an EIP-55 checksum, and one that does not
24409
+ // match is a mistyped address: refuse it as `recipient` here rather than
24410
+ // let the ABI coder throw its own error at the encode below.
24411
+ try {
24412
+ ethers.utils.getAddress(input.to);
24413
+ }
24414
+ catch (_a) {
24415
+ throw new SponsoredCallInputError('recipient');
24416
+ }
24417
+ if (ethers.BigNumber.from(input.to).isZero()) {
24418
+ throw new SponsoredCallInputError('zero-recipient');
24419
+ }
24420
+ const amount = toUint256(input.amount, 'amount');
24421
+ if (amount.isZero())
24422
+ throw new SponsoredCallInputError('zero-amount');
24423
+ return {
24424
+ kind: 'transfer',
24425
+ call: {
24426
+ to: input.stableAsset,
24427
+ data: ERC20$2.encodeFunctionData('transfer', [input.to, amount]),
24428
+ },
24429
+ };
24430
+ }
24374
24431
 
24375
24432
  const ADDRESS_RE$1 = /^0x[0-9a-fA-F]{40}$/;
24376
24433
  /**
@@ -25602,7 +25659,10 @@ function checkBatchOutcome(transactionId, expect, ports) {
25602
25659
  }
25603
25660
  if (read.kind === 'reverted')
25604
25661
  return 'not-deposited';
25605
- if (read.kind !== 'mined' || typeof read.txHash !== 'string') {
25662
+ // An empty hash is not an answer — `waitForMined` reads it the same way.
25663
+ if (read.kind !== 'mined' ||
25664
+ typeof read.txHash !== 'string' ||
25665
+ read.txHash === '') {
25606
25666
  return 'unknown';
25607
25667
  }
25608
25668
  try {
@@ -25619,10 +25679,60 @@ function checkBatchOutcome(transactionId, expect, ports) {
25619
25679
  }
25620
25680
  });
25621
25681
  }
25682
+ // ---------------------------------------------------------------------------
25683
+ // One sponsored call — withdraw, cancel, withdraw at expiry, transfer
25684
+ // ---------------------------------------------------------------------------
25685
+ /**
25686
+ * `checkBatchOutcome` for ONE sponsored call — ONE status read, no polling —
25687
+ * for an app's guard against sending the same request twice (a P2P transfer
25688
+ * whose earlier submission ended PENDING, say):
25689
+ *
25690
+ * - `executed` — mined, and the receipt proves the sender's operation
25691
+ * succeeded (`proveUserOperationInReceipt`);
25692
+ * - `not-executed` — definitively reverted (vendor status or the operation's
25693
+ * own flag);
25694
+ * - `unknown` — anything else, including a read or receipt that failed, or a
25695
+ * receipt that does not show the sender's operation.
25696
+ *
25697
+ * Only `not-executed` may let a new attempt proceed unprompted; `executed`
25698
+ * means the earlier one landed and the user should be told.
25699
+ */
25700
+ function checkSponsoredCallOutcome(transactionId, expect, ports) {
25701
+ return __awaiter(this, void 0, void 0, function* () {
25702
+ var _a;
25703
+ let read;
25704
+ try {
25705
+ read = yield ports.getStatus(transactionId);
25706
+ }
25707
+ catch (_b) {
25708
+ return 'unknown';
25709
+ }
25710
+ if (read.kind === 'reverted')
25711
+ return 'not-executed';
25712
+ // An empty hash is not an answer — `waitForMined` reads it the same way.
25713
+ if (read.kind !== 'mined' ||
25714
+ typeof read.txHash !== 'string' ||
25715
+ read.txHash === '') {
25716
+ return 'unknown';
25717
+ }
25718
+ try {
25719
+ const receipt = yield ports.getReceipt(read.txHash);
25720
+ const proof = proveUserOperationInReceipt(receipt, Object.assign(Object.assign({}, expect), { userOperationHash: (_a = read.userOperationHash) !== null && _a !== void 0 ? _a : null }));
25721
+ if (proof.kind === 'executed')
25722
+ return 'executed';
25723
+ if (proof.kind === 'reverted')
25724
+ return 'not-executed';
25725
+ return 'unknown';
25726
+ }
25727
+ catch (_c) {
25728
+ return 'unknown';
25729
+ }
25730
+ });
25731
+ }
25622
25732
  /**
25623
- * `submitBatch` for ONE sponsored `LendingPoolManager` call — the body of a
25624
- * `WithdrawFlow` `submitCall` port, and of an app's cancel or
25625
- * withdraw-at-expiry action. Same post, same polling policy, same outcome
25733
+ * `submitBatch` for ONE sponsored call — the body of a `WithdrawFlow`
25734
+ * `submitCall` port, of an app's cancel or withdraw-at-expiry action, and of
25735
+ * a P2P transfer (`encodeStableTransfer`). Same post, same polling policy, same outcome
25626
25736
  * table; the proof is `proveUserOperationInReceipt` (the sender's operation
25627
25737
  * succeeded) instead of the deposit proof, and an unproven receipt is
25628
25738
  * `call_unproven:<reason>`.
@@ -25638,7 +25748,8 @@ function checkBatchOutcome(transactionId, expect, ports) {
25638
25748
  * The call is checked against its kind before anything is posted — a known
25639
25749
  * kind, exactly `{ to, data }`, and `data` opening with that kind's selector —
25640
25750
  * and a mismatch is `BatchSubmissionError('call_mismatch', 'failed')`: nothing
25641
- * was sent. Build calls with `kasu.calls`, and this never fires.
25751
+ * was sent. Build calls with `kasu.calls` (or `encodeStableTransfer`), and
25752
+ * this never fires.
25642
25753
  *
25643
25754
  * As on the deposit path, nothing after the post ever produces `failed`.
25644
25755
  */
@@ -27434,6 +27545,82 @@ function maxNetRateCeiling(pools, feePercent) {
27434
27545
  return rate;
27435
27546
  }
27436
27547
 
27548
+ /**
27549
+ * A strategy's SHORT blurb, as plain text: the CMS `liteDescription` first,
27550
+ * else the first paragraph of the long `description`, else nothing.
27551
+ *
27552
+ * Lifted from kasu-ui (`pickShortDescription`, its deposit dialog and landing
27553
+ * strategy card) so every app shows the same line, and pinned to its output
27554
+ * for every live CMS row. The rules are facts about how the CMS is authored,
27555
+ * not formatting preferences:
27556
+ *
27557
+ * - `liteDescription` IS THE SHORT ONE. `description` is the credit
27558
+ * originator's several-paragraph profile; on a card it is a wall.
27559
+ * - `"Not Available"` IS A PLACEHOLDER, not editorial. Several rows carry it
27560
+ * verbatim, and it must read as missing, not print. Matched on the whole
27561
+ * trimmed value, case-insensitively. It applies to `liteDescription` only.
27562
+ * - EMPTY OR BLANK IS MISSING. The service maps a pool with no CMS record to
27563
+ * `''`.
27564
+ * - THE FIELDS HOLD MARKDOWN. `liteDescription` can open with a `**bold**`
27565
+ * lead, and the surfaces this feeds render plain text, so the markers are
27566
+ * stripped (bold, then italic, then inline code).
27567
+ * - AND HARD NEWLINES. Every run of whitespace becomes one space, so a
27568
+ * paragraph break inside the value is not a gap in a one-line slot.
27569
+ * - NOT SANITISED. "Plain text" means text to render AS text. HTML tags,
27570
+ * entities and link syntax (`<script>`, `&lt;`, `[a](javascript:…)`) pass
27571
+ * through unchanged, and removing markers can join fragments into a tag
27572
+ * (`<scr**i**pt>` becomes `<script>`). Render it as a text node, never as
27573
+ * HTML or markdown.
27574
+ *
27575
+ * Two refinements over the kasu-ui original, neither reachable by any live
27576
+ * row: the cleaned text is trimmed (a CRLF value no longer ends in a space),
27577
+ * and a `liteDescription` that cleans to nothing (`** **`) counts as missing,
27578
+ * so the result is never a blank string.
27579
+ *
27580
+ * This authors no string of its own. It selects and cleans editorial the CMS
27581
+ * already holds, and an absent blurb is `undefined`, never a fallback sentence:
27582
+ * what "no blurb" means is the render site's decision.
27583
+ *
27584
+ * Pure. A value that is not a string reads as missing.
27585
+ */
27586
+ function pickShortDescription(source) {
27587
+ var _a;
27588
+ const lite = asTrimmed(source.liteDescription);
27589
+ if (lite && !NOT_AVAILABLE.test(lite)) {
27590
+ const summary = toOneLine(lite);
27591
+ if (summary)
27592
+ return summary;
27593
+ }
27594
+ const long = asTrimmed(source.description);
27595
+ if (long) {
27596
+ const firstParagraph = (_a = long.split(PARAGRAPH_BREAK, 1)[0]) !== null && _a !== void 0 ? _a : long;
27597
+ return toOneLine(firstParagraph) || undefined;
27598
+ }
27599
+ return undefined;
27600
+ }
27601
+ const NOT_AVAILABLE = /^not available$/i;
27602
+ const PARAGRAPH_BREAK = /\n\s*\n/;
27603
+ function asTrimmed(value) {
27604
+ return typeof value === 'string' ? value.trim() : '';
27605
+ }
27606
+ /** Markers off, whitespace runs to one space, ends trimmed. `''` when nothing is left. */
27607
+ function toOneLine(text) {
27608
+ return stripInlineMarkdown(text).replace(/\s+/gu, ' ').trim();
27609
+ }
27610
+ /**
27611
+ * Inline markdown markers off, in the order bold → italic → code. The bold
27612
+ * passes consume every `**` / `__` pair first, so the italic patterns can match
27613
+ * a lone `*` / `_` without lookarounds.
27614
+ */
27615
+ function stripInlineMarkdown(input) {
27616
+ return input
27617
+ .replace(/\*\*(.+?)\*\*/g, '$1')
27618
+ .replace(/__(.+?)__/g, '$1')
27619
+ .replace(/\*(.+?)\*/g, '$1')
27620
+ .replace(/_(.+?)_/g, '$1')
27621
+ .replace(/`([^`]+)`/g, '$1');
27622
+ }
27623
+
27437
27624
  /**
27438
27625
  * High-level facade for browsing Kasu lending strategies (pools).
27439
27626
  *
@@ -27617,7 +27804,10 @@ class StrategiesFacade {
27617
27804
  return {
27618
27805
  id: pool.id,
27619
27806
  name: pool.poolName,
27807
+ subheading: nonEmptyOrUndefined(pool.subheading),
27620
27808
  description: pool.description,
27809
+ liteDescription: nonEmptyOrUndefined(pool.liteDescription),
27810
+ summary: pickShortDescription(pool),
27621
27811
  isActive: pool.isActive,
27622
27812
  apy: pool.apy,
27623
27813
  tvl: pool.totalValueLocked,
@@ -27632,6 +27822,16 @@ class StrategiesFacade {
27632
27822
  };
27633
27823
  }
27634
27824
  }
27825
+ /**
27826
+ * A CMS text field as a string, or `undefined` when it is absent, not a
27827
+ * string, or empty (blank included) — the CMS stores an emptied field as `''`, and an empty
27828
+ * subheading must read as "none", not render an empty line.
27829
+ */
27830
+ function nonEmptyOrUndefined(value) {
27831
+ return typeof value === 'string' && value.trim().length > 0
27832
+ ? value
27833
+ : undefined;
27834
+ }
27635
27835
  /**
27636
27836
  * A CMS "numeric" field as a finite number, or `null`. An empty or blank
27637
27837
  * string is UNAUTHORED, not zero.
@@ -29032,6 +29232,7 @@ exports.buildLoanAgreementSignMessage = buildLoanAgreementSignMessage;
29032
29232
  exports.cancelDeadline = cancelDeadline;
29033
29233
  exports.ceilToCents = ceilToCents;
29034
29234
  exports.checkBatchOutcome = checkBatchOutcome;
29235
+ exports.checkSponsoredCallOutcome = checkSponsoredCallOutcome;
29035
29236
  exports.classifyWalletFailure = classifyWalletFailure;
29036
29237
  exports.compareTrancheSeniority = compareTrancheSeniority;
29037
29238
  exports.computeSettlementWindow = computeSettlementWindow;
@@ -29045,6 +29246,7 @@ exports.encodeCancelDepositCall = encodeCancelDepositCall;
29045
29246
  exports.encodeCancelWithdrawalCall = encodeCancelWithdrawalCall;
29046
29247
  exports.encodeDepositData = encodeDepositData;
29047
29248
  exports.encodeRequestWithdrawalCall = encodeRequestWithdrawalCall;
29249
+ exports.encodeStableTransfer = encodeStableTransfer;
29048
29250
  exports.encodeWithdrawAtExpiryCall = encodeWithdrawAtExpiryCall;
29049
29251
  exports.epochRateToApy = epochRateToApy;
29050
29252
  exports.explorerAddressUrl = explorerAddressUrl;
@@ -29078,6 +29280,7 @@ exports.parseMinorUnits = parseMinorUnits;
29078
29280
  exports.parseTrancheBound = parseTrancheBound;
29079
29281
  exports.pickDefaultTrancheId = pickDefaultTrancheId;
29080
29282
  exports.pickHighestYieldTranche = pickHighestYieldTranche;
29283
+ exports.pickShortDescription = pickShortDescription;
29081
29284
  exports.poolAllTranchesFull = poolAllTranchesFull;
29082
29285
  exports.poolMaxApy = poolMaxApy;
29083
29286
  exports.projectedAnnualEarnings = projectedAnnualEarnings;