@kasufinance/kasu-sdk 2.6.0 → 2.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/README.md +167 -4
  2. package/dist/bundle.cjs.js +1389 -257
  3. package/dist/bundle.esm.js +1375 -258
  4. package/dist/domain/index.d.ts +5 -2
  5. package/dist/domain/index.js +3 -2
  6. package/dist/domain/index.js.map +1 -1
  7. package/dist/domain/requests.d.ts +42 -4
  8. package/dist/domain/requests.js +20 -4
  9. package/dist/domain/requests.js.map +1 -1
  10. package/dist/domain/revert-errors.d.ts +59 -0
  11. package/dist/domain/revert-errors.js +190 -0
  12. package/dist/domain/revert-errors.js.map +1 -0
  13. package/dist/domain/wallet-errors.d.ts +63 -8
  14. package/dist/domain/wallet-errors.js +115 -16
  15. package/dist/domain/wallet-errors.js.map +1 -1
  16. package/dist/facade/deposits.js +2 -1
  17. package/dist/facade/deposits.js.map +1 -1
  18. package/dist/facade/flows.d.ts +95 -0
  19. package/dist/facade/flows.js +116 -0
  20. package/dist/facade/flows.js.map +1 -0
  21. package/dist/facade/index.d.ts +3 -0
  22. package/dist/facade/index.js +3 -0
  23. package/dist/facade/index.js.map +1 -1
  24. package/dist/facade/kasu.d.ts +6 -2
  25. package/dist/facade/kasu.js +28 -3
  26. package/dist/facade/kasu.js.map +1 -1
  27. package/dist/facade/read-only.d.ts +12 -0
  28. package/dist/facade/read-only.js +13 -0
  29. package/dist/facade/read-only.js.map +1 -0
  30. package/dist/flows/deposit-flow.d.ts +332 -0
  31. package/dist/flows/deposit-flow.js +436 -0
  32. package/dist/flows/deposit-flow.js.map +1 -0
  33. package/dist/flows/flow.d.ts +72 -0
  34. package/dist/flows/flow.js +110 -0
  35. package/dist/flows/flow.js.map +1 -0
  36. package/dist/flows/index.d.ts +19 -0
  37. package/dist/flows/index.js +17 -0
  38. package/dist/flows/index.js.map +1 -0
  39. package/dist/flows/observable.d.ts +59 -0
  40. package/dist/flows/observable.js +81 -0
  41. package/dist/flows/observable.js.map +1 -0
  42. package/dist/flows/withdraw-flow.d.ts +99 -0
  43. package/dist/flows/withdraw-flow.js +83 -0
  44. package/dist/flows/withdraw-flow.js.map +1 -0
  45. package/dist/index.d.ts +1 -0
  46. package/dist/index.js +6 -0
  47. package/dist/index.js.map +1 -1
  48. package/dist/services/DataService/data-service.js +0 -1
  49. package/dist/services/DataService/data-service.js.map +1 -1
  50. package/package.json +8 -2
  51. package/src/domain/index.ts +10 -1
  52. package/src/domain/requests.ts +57 -4
  53. package/src/domain/revert-errors.ts +248 -0
  54. package/src/domain/wallet-errors.ts +135 -18
  55. package/src/facade/deposits.ts +2 -3
  56. package/src/facade/flows.ts +172 -0
  57. package/src/facade/index.ts +8 -0
  58. package/src/facade/kasu.ts +43 -3
  59. package/src/facade/read-only.ts +13 -0
  60. package/src/flows/deposit-flow.ts +884 -0
  61. package/src/flows/flow.ts +108 -0
  62. package/src/flows/index.ts +47 -0
  63. package/src/flows/observable.ts +97 -0
  64. package/src/flows/withdraw-flow.ts +210 -0
  65. package/src/index.ts +7 -0
  66. package/src/services/DataService/data-service.ts +0 -1
  67. package/dist/domain/au-minimum.test.d.ts +0 -1
  68. package/dist/domain/au-minimum.test.js +0 -202
  69. package/dist/domain/au-minimum.test.js.map +0 -1
  70. package/dist/domain/deposit-bounds.test.d.ts +0 -1
  71. package/dist/domain/deposit-bounds.test.js +0 -135
  72. package/dist/domain/deposit-bounds.test.js.map +0 -1
  73. package/dist/domain/loan-contract.test.d.ts +0 -1
  74. package/dist/domain/loan-contract.test.js +0 -255
  75. package/dist/domain/loan-contract.test.js.map +0 -1
  76. package/dist/domain/partners.test.d.ts +0 -1
  77. package/dist/domain/partners.test.js +0 -53
  78. package/dist/domain/partners.test.js.map +0 -1
  79. package/dist/domain/pools.test.d.ts +0 -1
  80. package/dist/domain/pools.test.js +0 -184
  81. package/dist/domain/pools.test.js.map +0 -1
  82. package/dist/domain/rates.test.d.ts +0 -1
  83. package/dist/domain/rates.test.js +0 -181
  84. package/dist/domain/rates.test.js.map +0 -1
  85. package/dist/domain/requests.test.d.ts +0 -1
  86. package/dist/domain/requests.test.js +0 -470
  87. package/dist/domain/requests.test.js.map +0 -1
  88. package/dist/domain/settlement.test.d.ts +0 -1
  89. package/dist/domain/settlement.test.js +0 -152
  90. package/dist/domain/settlement.test.js.map +0 -1
  91. package/dist/domain/tranche-display-name.test.d.ts +0 -1
  92. package/dist/domain/tranche-display-name.test.js +0 -58
  93. package/dist/domain/tranche-display-name.test.js.map +0 -1
  94. package/dist/domain/tranches.test.d.ts +0 -1
  95. package/dist/domain/tranches.test.js +0 -206
  96. package/dist/domain/tranches.test.js.map +0 -1
  97. package/dist/domain/wallet-errors.test.d.ts +0 -1
  98. package/dist/domain/wallet-errors.test.js +0 -71
  99. package/dist/domain/wallet-errors.test.js.map +0 -1
  100. package/dist/facade/config.test.d.ts +0 -1
  101. package/dist/facade/config.test.js +0 -216
  102. package/dist/facade/config.test.js.map +0 -1
  103. package/dist/facade/facade.test.d.ts +0 -1
  104. package/dist/facade/facade.test.js +0 -278
  105. package/dist/facade/facade.test.js.map +0 -1
  106. package/dist/services/Locking/calculate-apy.test.d.ts +0 -1
  107. package/dist/services/Locking/calculate-apy.test.js +0 -41
  108. package/dist/services/Locking/calculate-apy.test.js.map +0 -1
  109. package/dist/tests/sample.test.d.ts +0 -1
  110. package/dist/tests/sample.test.js +0 -59
  111. package/dist/tests/sample.test.js.map +0 -1
  112. package/src/domain/au-minimum.test.ts +0 -371
  113. package/src/domain/deposit-bounds.test.ts +0 -200
  114. package/src/domain/loan-contract.test.ts +0 -343
  115. package/src/domain/partners.test.ts +0 -83
  116. package/src/domain/pools.test.ts +0 -260
  117. package/src/domain/rates.test.ts +0 -254
  118. package/src/domain/requests.test.ts +0 -653
  119. package/src/domain/settlement.test.ts +0 -198
  120. package/src/domain/tranche-display-name.test.ts +0 -96
  121. package/src/domain/tranches.test.ts +0 -300
  122. package/src/domain/wallet-errors.test.ts +0 -100
  123. package/src/facade/config.test.ts +0 -265
  124. package/src/facade/facade.test.ts +0 -374
  125. package/src/services/Locking/calculate-apy.test.ts +0 -52
  126. package/src/tests/sample.test.ts +0 -63
@@ -16207,7 +16207,6 @@ class DataService {
16207
16207
  return __awaiter(this, void 0, void 0, function* () {
16208
16208
  var _a;
16209
16209
  const poolDelegateProfileAndHistoryDirectus = (yield this._directus.request(bs('PoolDelegateProfileAndHistory', {
16210
- // @ts-ignore Directus' deep-fields typing is fragile across versions
16211
16210
  fields: ['*', { otherPools: ['*'] }],
16212
16211
  })));
16213
16212
  const [poolNames, directusPoolNames] = yield Promise.all([
@@ -20838,12 +20837,12 @@ function toURLEncodedForm(data, options) {
20838
20837
  });
20839
20838
  }
20840
20839
 
20841
- const MAX_DEPTH = DEFAULT_FORM_DATA_MAX_DEPTH;
20840
+ const MAX_DEPTH$1 = DEFAULT_FORM_DATA_MAX_DEPTH;
20842
20841
 
20843
20842
  function throwIfDepthExceeded(index) {
20844
- if (index > MAX_DEPTH) {
20843
+ if (index > MAX_DEPTH$1) {
20845
20844
  throw new AxiosError$1(
20846
- 'FormData field is too deeply nested (' + index + ' levels). Max depth: ' + MAX_DEPTH,
20845
+ 'FormData field is too deeply nested (' + index + ' levels). Max depth: ' + MAX_DEPTH$1,
20847
20846
  AxiosError$1.ERR_FORM_DATA_DEPTH_EXCEEDED
20848
20847
  );
20849
20848
  }
@@ -24353,6 +24352,19 @@ const CHAIN_CONFIGS = {
24353
24352
  },
24354
24353
  };
24355
24354
 
24355
+ /**
24356
+ * The refusal every write path shares when the instance holds a Provider
24357
+ * rather than a Signer.
24358
+ *
24359
+ * One constant, because the message's job is to name the fix — `kasu.connect`
24360
+ * — and three near-identical wordings across the facades would be three
24361
+ * different pieces of advice for one problem. ethers' own
24362
+ * "sending a transaction requires a signer" arrives several frames deeper,
24363
+ * after the params have been encoded, and says nothing about how to get a
24364
+ * signer onto THIS object.
24365
+ */
24366
+ const READ_ONLY_MESSAGE = 'Kasu: this instance is read-only; call kasu.connect(signer) first';
24367
+
24356
24368
  /**
24357
24369
  * High-level facade for deposit and withdrawal operations.
24358
24370
  *
@@ -24381,7 +24393,7 @@ class DepositsFacade {
24381
24393
  */
24382
24394
  assertWritable() {
24383
24395
  if (this._isReadOnly) {
24384
- throw new Error('Kasu: this instance is read-only; call kasu.connect(signer) first');
24396
+ throw new Error(READ_ONLY_MESSAGE);
24385
24397
  }
24386
24398
  }
24387
24399
  /**
@@ -24409,49 +24421,1330 @@ class DepositsFacade {
24409
24421
  });
24410
24422
  }
24411
24423
  /**
24412
- * Submit a withdrawal request for a specific stable asset amount.
24424
+ * Submit a withdrawal request for a specific stable asset amount.
24425
+ */
24426
+ withdraw(params) {
24427
+ return __awaiter(this, void 0, void 0, function* () {
24428
+ this.assertWritable();
24429
+ return yield this._userLending.requestWithdrawalInAsset(params.poolId, params.trancheId, params.amount);
24430
+ });
24431
+ }
24432
+ /**
24433
+ * Withdraw the entire balance from a tranche.
24434
+ */
24435
+ withdrawMax(poolId, trancheId, userAddress) {
24436
+ return __awaiter(this, void 0, void 0, function* () {
24437
+ this.assertWritable();
24438
+ return yield this._userLending.requestWithdrawalMax(poolId, trancheId, userAddress);
24439
+ });
24440
+ }
24441
+ /**
24442
+ * Build KYC signature parameters for the Nexera verification flow.
24443
+ *
24444
+ * The integrator is responsible for sending these to their backend,
24445
+ * which obtains the signature from Nexera's signing service.
24446
+ */
24447
+ buildKycParams(userAddress) {
24448
+ return this._userLending.buildKycSignatureParams(userAddress, this._chainId);
24449
+ }
24450
+ /**
24451
+ * Check whether the pool is currently in a clearing period
24452
+ * (deposits/withdrawals are temporarily paused).
24453
+ */
24454
+ isClearingPending(poolId) {
24455
+ return __awaiter(this, void 0, void 0, function* () {
24456
+ return yield this._userLending.isClearingPending(poolId);
24457
+ });
24458
+ }
24459
+ /**
24460
+ * Get the current epoch number.
24461
+ */
24462
+ getCurrentEpoch() {
24463
+ return __awaiter(this, void 0, void 0, function* () {
24464
+ return yield this._userLending.getCurrentEpoch();
24465
+ });
24466
+ }
24467
+ }
24468
+
24469
+ /**
24470
+ * The tiny observable-state kernel both flows are built on.
24471
+ *
24472
+ * Deliberately not a framework store: no React, no signals, no dependency.
24473
+ * A flow is a long asynchronous run that has to publish every transition to
24474
+ * whatever is rendering it, and has to be able to abandon a run that the
24475
+ * consumer walked away from. That is the whole requirement, and it is 60 lines.
24476
+ *
24477
+ * The `generation` counter is the abandonment mechanism. `reset()` bumps it;
24478
+ * an in-flight run captured the previous value, so every later `patch` it
24479
+ * attempts is dropped and every `isCurrent` checkpoint tells it to stop. It is
24480
+ * what makes `reset()` safe to call mid-flight — the run cannot resurrect a
24481
+ * dead state or, worse, drive the UI back into a phase the consumer has left.
24482
+ */
24483
+ class FlowStore {
24484
+ constructor(_initial) {
24485
+ this._initial = _initial;
24486
+ this._generation = 0;
24487
+ this._listeners = new Set();
24488
+ this._state = _initial;
24489
+ }
24490
+ get state() {
24491
+ return this._state;
24492
+ }
24493
+ /** The run token an in-flight pipeline captures at `start()`. */
24494
+ get generation() {
24495
+ return this._generation;
24496
+ }
24497
+ /** False once `reset()` (or a later `start()`) has superseded `token`. */
24498
+ isCurrent(token) {
24499
+ return this._generation === token;
24500
+ }
24501
+ /**
24502
+ * Subscribe to every transition. Returns the unsubscribe function; calling
24503
+ * it twice is harmless.
24504
+ *
24505
+ * Listeners are NOT called on subscribe — read `state` for the current
24506
+ * value. A listener that throws is not allowed to break the pipeline or
24507
+ * starve the listeners registered after it, so throws are swallowed.
24508
+ */
24509
+ subscribe(listener) {
24510
+ this._listeners.add(listener);
24511
+ return () => {
24512
+ this._listeners.delete(listener);
24513
+ };
24514
+ }
24515
+ /**
24516
+ * Merge `next` into the state and notify, unless `token` names a
24517
+ * superseded run.
24518
+ */
24519
+ patch(next, token) {
24520
+ if (token !== undefined && !this.isCurrent(token))
24521
+ return;
24522
+ this._state = Object.assign(Object.assign({}, this._state), next);
24523
+ for (const listener of [...this._listeners]) {
24524
+ try {
24525
+ listener(this._state);
24526
+ }
24527
+ catch (_a) {
24528
+ // A consumer's rendering fault is its own problem; the pipeline
24529
+ // owns real money and keeps going.
24530
+ }
24531
+ }
24532
+ }
24533
+ /**
24534
+ * Back to the initial state, abandoning any in-flight run.
24535
+ *
24536
+ * Subscribers are kept: the consumer that was watching the last run is the
24537
+ * one that will watch the next.
24538
+ */
24539
+ reset() {
24540
+ this._generation += 1;
24541
+ this.patch(this._initial);
24542
+ }
24543
+ /** Claim the next run token. Any earlier run is abandoned. */
24544
+ beginRun() {
24545
+ this._generation += 1;
24546
+ return this._generation;
24547
+ }
24548
+ }
24549
+
24550
+ /**
24551
+ * The run lifecycle both flows share: one run at a time, every transition
24552
+ * published, and a `reset()` that is safe to call in the middle of one.
24553
+ *
24554
+ * `DepositFlow` and `WithdrawFlow` each carried their own copy of this — the
24555
+ * re-entrancy guard, the `try/finally` around the run, `state`, `isRunning`,
24556
+ * `subscribe`, `reset`. Two copies of a guard is two places for a guard to be
24557
+ * wrong, on the money path, which is the same reason the pipelines themselves
24558
+ * moved out of the applications.
24559
+ *
24560
+ * ## The guard and the run token are separate, and both matter
24561
+ *
24562
+ * `_runToken` is the re-entrancy guard: claimed synchronously, before the
24563
+ * first `await`, so a double tap cannot launch two pipelines. The store's
24564
+ * `generation` is the abandonment mechanism: an in-flight run holds the value
24565
+ * it claimed, and every `patch` and `isCurrent` checkpoint it makes after
24566
+ * `reset()` is dropped.
24567
+ *
24568
+ * `reset()` releases the guard IMMEDIATELY rather than waiting for the
24569
+ * abandoned run to notice. It has to: a run parked on a wallet prompt that
24570
+ * never answers is a promise that may never settle, and a consumer that has
24571
+ * left that screen must be able to start a new run in the same tick. The
24572
+ * abandoned run's `finally` therefore releases the guard only if it still
24573
+ * holds it — by then a NEW run may own it, and clearing it there would let a
24574
+ * third run start alongside the second.
24575
+ */
24576
+ class Flow {
24577
+ constructor(initial) {
24578
+ /**
24579
+ * The token of the run currently holding the guard, or `null` when none
24580
+ * does. Not a boolean: releasing the guard has to be attributable to the
24581
+ * run that claimed it.
24582
+ */
24583
+ this._runToken = null;
24584
+ this._store = new FlowStore(initial);
24585
+ }
24586
+ /** The current state. Every transition is also published to `subscribe`. */
24587
+ get state() {
24588
+ return this._store.state;
24589
+ }
24590
+ /** True while a run is in flight, including while parked. */
24591
+ get isRunning() {
24592
+ return this._runToken !== null;
24593
+ }
24594
+ /**
24595
+ * Observe every transition. Returns the unsubscribe function.
24596
+ *
24597
+ * The listener is not called on subscribe; read `state` for the value it
24598
+ * starts from.
24599
+ */
24600
+ subscribe(listener) {
24601
+ return this._store.subscribe(listener);
24602
+ }
24603
+ /**
24604
+ * Run the pipeline. Resolves when it reaches a terminal phase — it does
24605
+ * not reject, because every outcome a consumer can act on is in `state`.
24606
+ *
24607
+ * A second call while one is in flight is a no-op: one submission per
24608
+ * intent, however many times the button is pressed.
24609
+ */
24610
+ start(input) {
24611
+ return __awaiter(this, void 0, void 0, function* () {
24612
+ if (this._runToken !== null)
24613
+ return;
24614
+ this._store.reset();
24615
+ const token = this._store.beginRun();
24616
+ this._runToken = token;
24617
+ try {
24618
+ yield this._run(input, token);
24619
+ }
24620
+ finally {
24621
+ if (this._runToken === token)
24622
+ this._runToken = null;
24623
+ }
24624
+ });
24625
+ }
24626
+ /**
24627
+ * Back to `idle`, abandoning any run in flight: its remaining transitions
24628
+ * are dropped, whatever it is parked on is unparked, and the guard is
24629
+ * released at once so `start()` is accepted in the SAME tick. Subscribers
24630
+ * are kept — the consumer watching the last run is the one watching the
24631
+ * next.
24632
+ */
24633
+ reset() {
24634
+ this._runToken = null;
24635
+ // Bump the generation BEFORE unparking, so the abandoned run's next
24636
+ // checkpoint already reads as superseded.
24637
+ this._store.reset();
24638
+ this._onAbandon();
24639
+ }
24640
+ /**
24641
+ * Hook for a flow that parks: unpark it. Called by `reset()` after the
24642
+ * generation has been bumped, so anything the abandoned run does with the
24643
+ * result is dropped.
24644
+ */
24645
+ _onAbandon() {
24646
+ // A flow with nothing parked has nothing to unpark.
24647
+ }
24648
+ }
24649
+
24650
+ /** Narrow the backend's loose `contractType` string to the encoded union. */
24651
+ function asContractType(raw) {
24652
+ return raw === 'exempt' ? 'exempt' : 'retail';
24653
+ }
24654
+ /**
24655
+ * Parse the server's JSON-string `formattedMessage` into a tree. Returns
24656
+ * `null` on parse failure so a renderer can fall back to the plaintext.
24657
+ */
24658
+ function parseFormattedMessage(raw) {
24659
+ try {
24660
+ const parsed = JSON.parse(raw);
24661
+ if (parsed && typeof parsed === 'object')
24662
+ return parsed;
24663
+ return null;
24664
+ }
24665
+ catch (_a) {
24666
+ return null;
24667
+ }
24668
+ }
24669
+ // ---------------------------------------------------------------------------
24670
+ // The on-chain `depositData` blob
24671
+ // ---------------------------------------------------------------------------
24672
+ /**
24673
+ * Pack the contract version and type into the `versionType` word.
24674
+ *
24675
+ * ```
24676
+ * high byte = contract version (>= 1)
24677
+ * low byte = 0 for retail, 1 for exempt
24678
+ * ```
24679
+ */
24680
+ function buildContractVersionType(contractVersion, contractType) {
24681
+ return (contractVersion << 8) + (contractType === 'retail' ? 0 : 1);
24682
+ }
24683
+ /**
24684
+ * Build the on-chain `depositData` blob that `requestDepositWithKyc` expects.
24685
+ *
24686
+ * The KasuController decodes the bytes as
24687
+ * `(bytes signature, uint256 timestamp, uint256 versionType)` and uses the
24688
+ * embedded acceptance signature to verify — retrospectively, via the
24689
+ * agreements service `/contract/resolve` — that the lender signed the
24690
+ * loan-contract text. The ABI tuple and the packing are consensus-critical:
24691
+ * these bytes go on chain.
24692
+ *
24693
+ * kasu-ui encodes this with viem, kasu-mobile with ethers v5 (viem is not
24694
+ * available on Expo). This is the ethers v5 implementation, and
24695
+ * `loan-contract.test.ts` pins its output byte-for-byte against fixtures
24696
+ * produced by the viem version, so the two apps can never diverge here.
24697
+ *
24698
+ * @param args.signature EIP-191 signature from the lender accepting
24699
+ * `contractMessage`, as a 0x-prefixed hex string.
24700
+ * @param args.timestamp ms-epoch from the contract response.
24701
+ */
24702
+ function encodeDepositData(args) {
24703
+ const versionType = buildContractVersionType(args.contractVersion, args.contractType);
24704
+ return ethers.ethers.utils.defaultAbiCoder.encode(['bytes', 'uint256', 'uint256'], [
24705
+ args.signature,
24706
+ ethers.ethers.BigNumber.from(args.timestamp),
24707
+ ethers.ethers.BigNumber.from(versionType),
24708
+ ]);
24709
+ }
24710
+ // ---------------------------------------------------------------------------
24711
+ // The signed messages
24712
+ // ---------------------------------------------------------------------------
24713
+ const MONTH_NAMES = [
24714
+ 'January',
24715
+ 'February',
24716
+ 'March',
24717
+ 'April',
24718
+ 'May',
24719
+ 'June',
24720
+ 'July',
24721
+ 'August',
24722
+ 'September',
24723
+ 'October',
24724
+ 'November',
24725
+ 'December',
24726
+ ];
24727
+ /**
24728
+ * Format a unix timestamp as `{day} {MonthName} {yyyy}, {HH}:{mm}` in UTC.
24729
+ * Day is non-padded; hour and minute are zero-padded to two digits (24h). A
24730
+ * timestamp with >= 13 digits is treated as milliseconds, otherwise as seconds
24731
+ * — the same auto-detection kasu-backend applies.
24732
+ *
24733
+ * Deliberately a manual formatter with English month names: no locale, no
24734
+ * `Intl`, so the output is byte-identical across runtimes and time zones. This
24735
+ * is not a display date. It goes inside a signed message.
24736
+ *
24737
+ * e.g. 1785313320000 → `"29 July 2026, 08:22"`
24738
+ */
24739
+ function formatSignTimestampUtc(timestamp) {
24740
+ const ms = timestamp.toString().length >= 13 ? timestamp : timestamp * 1000;
24741
+ const date = new Date(ms);
24742
+ const day = date.getUTCDate();
24743
+ const month = MONTH_NAMES[date.getUTCMonth()];
24744
+ const year = date.getUTCFullYear();
24745
+ const hours = String(date.getUTCHours()).padStart(2, '0');
24746
+ const minutes = String(date.getUTCMinutes()).padStart(2, '0');
24747
+ return `${day} ${month} ${year}, ${hours}:${minutes}`;
24748
+ }
24749
+ /**
24750
+ * The 4-line human-readable message a lender signs to generate their loan
24751
+ * agreement for review — `POST /contract/generate`.
24752
+ *
24753
+ * ⚠️ BYTE-EXACT PROTOCOL STRING. kasu-backend rebuilds this string from the
24754
+ * request body and verifies the signature against it, so the wording,
24755
+ * ordering, separators, line breaks and date format are all part of the wire
24756
+ * contract. The separator between the line-2 fields is a MIDDLE DOT U+00B7
24757
+ * (·) with a single space on each side; the four lines are joined with `\n`.
24758
+ *
24759
+ * The backend takes this format only when all four display fields are present
24760
+ * and non-empty, and it cross-checks `amountLabel`'s leading number against
24761
+ * the `depositAmount` it was sent (thousands separators stripped) — a message
24762
+ * that states an amount other than the one being executed is refused.
24763
+ */
24764
+ function buildLoanAgreementSignMessage(p) {
24765
+ return [
24766
+ 'Generate my Loan Agreement for review:',
24767
+ `${p.strategyName} · ${p.region} · ${p.optionName} · ${p.amountLabel}.`,
24768
+ `Request made ${formatSignTimestampUtc(p.timestamp)} UTC.`,
24769
+ 'This request does not commit me to lend.',
24770
+ ].join('\n');
24771
+ }
24772
+ /**
24773
+ * The legacy `/contract/generate` and `/contract/resolve` message.
24774
+ *
24775
+ * ⚠️ BYTE-EXACT PROTOCOL STRING. kasu-backend rebuilds it as
24776
+ * `` `I request contract content for ${address} at ${timestamp}.` `` from the
24777
+ * `address` and `timestamp` fields of the request body — so the string signed
24778
+ * and the body sent must agree exactly, INCLUDING the address casing. This
24779
+ * builder lowercases, and the request body must carry the same lowercased
24780
+ * address; that is what both apps signing this format do today.
24781
+ *
24782
+ * The backend takes this path whenever the four human-readable display fields
24783
+ * are absent, and documents it as permanent until the legacy app is
24784
+ * decommissioned. `/contract/resolve` has no other format — every consumer
24785
+ * signs this one to retrieve an existing agreement.
24786
+ *
24787
+ * @param timestampMs ms-epoch, and the same value sent as the body's
24788
+ * `timestamp`.
24789
+ */
24790
+ function buildLegacyContractRequestMessage(address, timestampMs) {
24791
+ return `I request contract content for ${address.toLowerCase()} at ${timestampMs}.`;
24792
+ }
24793
+ /**
24794
+ * The `POST /contract/fullname` message.
24795
+ *
24796
+ * ⚠️ BYTE-EXACT PROTOCOL STRING, on the same terms as
24797
+ * `buildLegacyContractRequestMessage`: kasu-backend rebuilds
24798
+ * `` `I request my full name for ${address} at ${timestamp}.` `` from the
24799
+ * request body and verifies the signature against it, so the body must carry
24800
+ * the same lowercased address this builder signs.
24801
+ *
24802
+ * @param timestampMs ms-epoch, and the same value sent as the body's
24803
+ * `timestamp`.
24804
+ */
24805
+ function buildFullNameRequestMessage(address, timestampMs) {
24806
+ return `I request my full name for ${address.toLowerCase()} at ${timestampMs}.`;
24807
+ }
24808
+
24809
+ // ---------------------------------------------------------------------------
24810
+ // The selector tables
24811
+ // ---------------------------------------------------------------------------
24812
+ /**
24813
+ * OpenZeppelin v5's ERC-20 errors. They are not in either Kasu ABI because no
24814
+ * Kasu contract declares them — they arrive from the TOKEN, through the
24815
+ * manager's `transferFrom`, and they are the modern spelling of the string
24816
+ * reverts below.
24817
+ */
24818
+ const ERC20_ERROR_ABI = [
24819
+ 'error ERC20InsufficientBalance(address sender, uint256 balance, uint256 needed)',
24820
+ 'error ERC20InsufficientAllowance(address spender, uint256 allowance, uint256 needed)',
24821
+ ];
24822
+ /**
24823
+ * The protocol errors that mean the ERC-20 transfer failed rather than the
24824
+ * protocol refusing.
24825
+ *
24826
+ * `SafeERC20FailedOperation` is OpenZeppelin's wrapper around a token call
24827
+ * that returned false or reverted, which on this path IS the `transferFrom`;
24828
+ * `AddressInsufficientBalance` is its native-value sibling. Both belong with
24829
+ * the balance case, not with the pool conditions — a lender who sees one of
24830
+ * them really does need to fund or re-approve.
24831
+ */
24832
+ const ERC20_FAMILY_NAMES = new Set([
24833
+ 'AddressInsufficientBalance',
24834
+ 'ERC20InsufficientAllowance',
24835
+ 'ERC20InsufficientBalance',
24836
+ 'SafeERC20FailedOperation',
24837
+ ]);
24838
+ /** Solidity's `Error(string)`, the selector every `require` reason arrives under. */
24839
+ const STRING_REVERT_SELECTOR = '0x08c379a0';
24840
+ /**
24841
+ * The `require` reasons that are a balance or an allowance, across the token
24842
+ * implementations in circulation: OpenZeppelin v4's sentences, and the
24843
+ * shorthand the minimal implementations use.
24844
+ */
24845
+ const ERC20_REASON_PATTERNS = [
24846
+ /exceeds\s+balance/,
24847
+ /exceeds\s+allowance/,
24848
+ /insufficient\s+balance/,
24849
+ /insufficient\s+allowance/,
24850
+ /transfer_from_failed/,
24851
+ /transferfrom\s+failed/,
24852
+ /low-level\s+call\s+failed/,
24853
+ ];
24854
+ /**
24855
+ * selector → error name, built once on first use.
24856
+ *
24857
+ * Lazily, because a consumer that never sees a revert should not pay for three
24858
+ * `Interface` constructions at import. The two Kasu tables are the GENERATED
24859
+ * typechain ABIs, so this cannot drift from the contracts: regenerate the ABIs
24860
+ * and the new errors are recognised with no edit here.
24861
+ */
24862
+ let selectorTable = null;
24863
+ function selectors() {
24864
+ if (selectorTable)
24865
+ return selectorTable;
24866
+ const table = new Map();
24867
+ const interfaces = [
24868
+ // The manager the deposit is called against, and the allow list it
24869
+ // delegates the KYC and block-window checks to.
24870
+ ILendingPoolManagerAbi__factory.createInterface(),
24871
+ IKasuAllowListAbi__factory.createInterface(),
24872
+ new ethers.ethers.utils.Interface(ERC20_ERROR_ABI),
24873
+ ];
24874
+ for (const iface of interfaces) {
24875
+ for (const fragment of Object.values(iface.errors)) {
24876
+ // First writer wins: the same name declared by both ABIs is the
24877
+ // same selector, so the duplicate would be a no-op anyway.
24878
+ const sighash = iface.getSighash(fragment);
24879
+ if (!table.has(sighash))
24880
+ table.set(sighash, fragment.name);
24881
+ }
24882
+ }
24883
+ selectorTable = table;
24884
+ return table;
24885
+ }
24886
+ // ---------------------------------------------------------------------------
24887
+ // Getting the bytes out of the envelope
24888
+ // ---------------------------------------------------------------------------
24889
+ /**
24890
+ * How deep to look for the revert data. Providers nest it differently —
24891
+ * ethers' own error wraps the provider's, which wraps the RPC body — and a
24892
+ * bound is what keeps a self-referencing error object from being walked
24893
+ * forever.
24894
+ */
24895
+ const MAX_DEPTH = 6;
24896
+ /** The keys ethers and the providers under it put a wrapped error on. */
24897
+ const NESTED_KEYS = [
24898
+ 'data',
24899
+ 'error',
24900
+ 'originalError',
24901
+ 'info',
24902
+ 'cause',
24903
+ 'body',
24904
+ ];
24905
+ /** At least a 4-byte selector, and a whole number of bytes. */
24906
+ function isRevertData(value) {
24907
+ return (value.length >= 10 &&
24908
+ value.length % 2 === 0 &&
24909
+ /^0x[0-9a-fA-F]+$/.test(value));
24910
+ }
24911
+ /**
24912
+ * Find the revert data on an error, wherever the provider left it.
24913
+ *
24914
+ * ethers v5 raises `UNPREDICTABLE_GAS_LIMIT` with the provider's own error
24915
+ * attached, and each provider nests the bytes one layer differently:
24916
+ * `error.data`, `error.error.data`, `error.data.originalError.data`, or only
24917
+ * inside the JSON `body` of a `SERVER_ERROR`. Rather than enumerate the
24918
+ * products, walk a fixed set of keys to a fixed depth and take the first value
24919
+ * that is shaped like revert data.
24920
+ */
24921
+ function extractRevertData(err) {
24922
+ return walk(err, 0);
24923
+ }
24924
+ function walk(node, depth) {
24925
+ if (node == null || depth > MAX_DEPTH)
24926
+ return null;
24927
+ if (typeof node === 'string') {
24928
+ if (isRevertData(node))
24929
+ return node;
24930
+ // A `SERVER_ERROR`'s body: the RPC's JSON reply, verbatim.
24931
+ if (!node.startsWith('{'))
24932
+ return null;
24933
+ try {
24934
+ return walk(JSON.parse(node), depth + 1);
24935
+ }
24936
+ catch (_a) {
24937
+ return null;
24938
+ }
24939
+ }
24940
+ if (typeof node !== 'object')
24941
+ return null;
24942
+ const record = node;
24943
+ for (const key of NESTED_KEYS) {
24944
+ const found = walk(record[key], depth + 1);
24945
+ if (found)
24946
+ return found;
24947
+ }
24948
+ return null;
24949
+ }
24950
+ // ---------------------------------------------------------------------------
24951
+ // Decoding
24952
+ // ---------------------------------------------------------------------------
24953
+ /**
24954
+ * Decode a failed call's revert into the error the contract named, or `null`
24955
+ * when there is nothing recognisable to name.
24956
+ *
24957
+ * `null` is the honest answer for an unknown selector, an unrelated `require`
24958
+ * string, or an error with no revert data at all — the caller keeps whatever
24959
+ * it did before rather than inventing a diagnosis from bytes it cannot read.
24960
+ */
24961
+ function decodeRevert(err) {
24962
+ const data = extractRevertData(err);
24963
+ if (!data)
24964
+ return null;
24965
+ const selector = data.slice(0, 10).toLowerCase();
24966
+ if (selector === STRING_REVERT_SELECTOR) {
24967
+ const reason = decodeStringRevert(data);
24968
+ if (reason === null)
24969
+ return null;
24970
+ return isErc20Reason(reason)
24971
+ ? { name: 'Error', family: 'erc20', reason }
24972
+ : null;
24973
+ }
24974
+ const name = selectors().get(selector);
24975
+ if (!name)
24976
+ return null;
24977
+ return {
24978
+ name,
24979
+ family: ERC20_FAMILY_NAMES.has(name) ? 'erc20' : 'protocol',
24980
+ };
24981
+ }
24982
+ function decodeStringRevert(data) {
24983
+ try {
24984
+ const [reason] = ethers.ethers.utils.defaultAbiCoder.decode(['string'], `0x${data.slice(10)}`);
24985
+ return reason;
24986
+ }
24987
+ catch (_a) {
24988
+ return null;
24989
+ }
24990
+ }
24991
+ function isErc20Reason(reason) {
24992
+ const lower = reason.toLowerCase();
24993
+ return ERC20_REASON_PATTERNS.some((pattern) => pattern.test(lower));
24994
+ }
24995
+
24996
+ /**
24997
+ * Wallet and RPC error predicates — pure functions over `unknown`.
24998
+ *
24999
+ * Every consumer has to tell three things apart when a write fails: the lender
25000
+ * changed their mind, the call would revert, and everything else. The first
25001
+ * two must never be reported as a failure the lender should retry or contact
25002
+ * support about, and each wallet spells them differently, so the shapes are
25003
+ * enumerated once here.
25004
+ *
25005
+ * Lifted from kasu-ui's `src/lib/web3/is-user-rejected.ts` and kasu-mobile's
25006
+ * `src/features/lending/lib/errors.ts` — and now the UNION of the two, so
25007
+ * neither app has to keep a wrapper on top of this one.
25008
+ */
25009
+ /**
25010
+ * Did the lender reject the request in their wallet?
25011
+ *
25012
+ * Providers surface a rejection in different shapes, and the union of them is
25013
+ * the point of this function existing once:
25014
+ * - MetaMask and most EIP-1193 wallets: `code: 4001`
25015
+ * - Coinbase Wallet and ethers v5: `code: 'ACTION_REJECTED'`
25016
+ * - viem: a `UserRejectedRequestError` name
25017
+ * - WalletConnect and some Privy paths: `Error('User rejected the request')`
25018
+ * - a provider error WRAPPED by another layer, carrying the real code and
25019
+ * message on a nested `error` — the shape Privy's embedded wallet
25020
+ * surfaces on Expo, where the outer object says nothing useful
25021
+ * - ethers' own `reason` field, which is often the only place the text lands
25022
+ * - a PLAIN OBJECT carrying `message`, which `String(err)` would read as
25023
+ * `"[object Object]"`
25024
+ *
25025
+ * Previously kasu-ui's implementation verbatim, with kasu-mobile keeping its
25026
+ * own superset on top. That is precisely the drift this layer exists to stop —
25027
+ * a rejection kasu-mobile recognised and kasu-ui did not was reported to the
25028
+ * same lender as a failure on one app and a cancellation on the other. The
25029
+ * union lives here; the mobile wrapper goes.
25030
+ *
25031
+ * ## The text is read for a SUBJECT, not for a keyword
25032
+ *
25033
+ * The words alone are not the signal. "Declined" and "request rejected" are
25034
+ * also what a rate limiter, a risk engine and a KYC decision say, and those
25035
+ * arrive wrapped in exactly the same envelope a wallet error does — ethers'
25036
+ * `SERVER_ERROR` around `-32603` carries the upstream body on a nested
25037
+ * `error.message`, so `{ error: { message: 'request rejected: rate limit
25038
+ * exceeded' } }` is an RPC refusing to serve us, not a lender refusing to
25039
+ * sign. Reporting one as the other tells a lender they cancelled something
25040
+ * they never saw, and DROPS the real error on the floor.
25041
+ *
25042
+ * So a rejection is a machine-readable wallet code, or a sentence that names
25043
+ * the party who did it: "user rejected", "declined by the user", "cancelled by
25044
+ * the wallet". A bare "declined" is not a rejection, whatever else is on the
25045
+ * envelope.
25046
+ *
25047
+ * Within that rule, being generous is the safe direction: calling a genuine
25048
+ * fault a cancellation costs a lender one retry, while calling a deliberate
25049
+ * rejection a failure sends them to support to report a bug that does not
25050
+ * exist.
25051
+ */
25052
+ function isUserRejected(err) {
25053
+ var _a;
25054
+ if (!err)
25055
+ return false;
25056
+ if (typeof err === 'object') {
25057
+ const e = err;
25058
+ if (isRejectionCode(e.code))
25059
+ return true;
25060
+ // A wrapped provider error: the outer layer's code is its own, the
25061
+ // inner one is the wallet's.
25062
+ if (isRejectionCode((_a = e.error) === null || _a === void 0 ? void 0 : _a.code))
25063
+ return true;
25064
+ }
25065
+ const lower = rejectionText(err).toLowerCase();
25066
+ return USER_REJECTION_PATTERNS.some((pattern) => pattern.test(lower));
25067
+ }
25068
+ function isRejectionCode(code) {
25069
+ return code === 4001 || code === 'ACTION_REJECTED';
25070
+ }
25071
+ /**
25072
+ * The wordings that name the wallet or the person at it.
25073
+ *
25074
+ * Each one carries a SUBJECT — the identifier a wallet library uses, or the
25075
+ * verb with the party who performed it. That is what separates a lender
25076
+ * pressing Reject from a server declining to answer, and it is why none of
25077
+ * these is a bare "declined" or "rejected".
25078
+ */
25079
+ const USER_REJECTION_PATTERNS = [
25080
+ // Machine-readable markers that only a wallet layer emits, arriving as
25081
+ // text because something in between stringified the error.
25082
+ /action_rejected/,
25083
+ /user_rejected/,
25084
+ /userrejectedrequest/,
25085
+ // "the user did it", in the orders the wallets write it.
25086
+ /user\s+(?:has\s+)?(?:rejected|denied|declined|refused|cancell?ed)/,
25087
+ /(?:rejected|denied|declined|refused|cancell?ed)\s+by\s+(?:the\s+)?(?:user|wallet|signer|owner)/,
25088
+ // The wallet as the subject, which is how some embedded wallets word it.
25089
+ /(?:wallet|signer)\s+(?:rejected|denied|declined|refused)/,
25090
+ ];
25091
+ /**
25092
+ * Every place a wallet might have put the words: the message, ethers' `reason`,
25093
+ * the error's `name` (viem puts the whole signal there), and a wrapped error's
25094
+ * own three. Joined rather than picked, because which one carries the text
25095
+ * depends on how many layers wrapped it.
25096
+ *
25097
+ * A separator is used rather than a bare space: two fields must not be able to
25098
+ * form a phrase across the join that neither of them said.
25099
+ */
25100
+ function rejectionText(err) {
25101
+ var _a, _b, _c;
25102
+ if (typeof err !== 'object' || err === null)
25103
+ return String(err);
25104
+ const e = err;
25105
+ return [
25106
+ e.message,
25107
+ e.reason,
25108
+ e.name,
25109
+ (_a = e.error) === null || _a === void 0 ? void 0 : _a.message,
25110
+ (_b = e.error) === null || _b === void 0 ? void 0 : _b.reason,
25111
+ (_c = e.error) === null || _c === void 0 ? void 0 : _c.name,
25112
+ ]
25113
+ .filter((part) => typeof part === 'string')
25114
+ .join(' | ');
25115
+ }
25116
+ /**
25117
+ * Did the wallet or RPC signal that the on-chain call would revert?
25118
+ *
25119
+ * ethers v5 raises `UNPREDICTABLE_GAS_LIMIT` when gas estimation reverts —
25120
+ * most often an underlying `transferFrom` failing on an insufficient balance
25121
+ * or allowance. Distinct from a rejection: nothing was refused by the lender,
25122
+ * the transaction simply cannot succeed as composed, so the caller should
25123
+ * re-check its preconditions rather than invite a retry.
25124
+ */
25125
+ function isUnpredictableGas(err) {
25126
+ if (!err || typeof err !== 'object')
25127
+ return false;
25128
+ return err.code === 'UNPREDICTABLE_GAS_LIMIT';
25129
+ }
25130
+ /**
25131
+ * The `cancelled` / `failed` split, for a WALLET call on any step of any flow.
25132
+ *
25133
+ * A lender who pressed Reject is not a fault. Reporting one as the other is
25134
+ * how a support queue fills with people who did exactly what they meant to.
25135
+ *
25136
+ * Generic in the step so both flows share one implementation — it was
25137
+ * duplicated byte-for-byte in each of them, which is the same drift this
25138
+ * layer exists to stop, one level up.
25139
+ *
25140
+ * Only pass it an error a WALLET produced. An HTTP port's throw is always a
25141
+ * `failed`: the lender's wallet was not involved in it, so a backend that
25142
+ * happens to word a refusal "declined" must never be shown to them as
25143
+ * something they did.
25144
+ */
25145
+ function classifyWalletFailure(step, err) {
25146
+ return isUserRejected(err)
25147
+ ? { step, reason: 'cancelled' }
25148
+ : { step, reason: 'failed', error: err };
25149
+ }
25150
+
25151
+ /** Generated agreements are valid for five minutes upstream. */
25152
+ const CONTRACT_TTL_MS = 5 * 60 * 1000;
25153
+ /** What a run without a spender, from either source, fails with. */
25154
+ const NO_SPENDER_MESSAGE = 'DepositFlow: no ERC-20 spender; build the flow with kasu.flows.deposit() or pass `spender` on the input';
25155
+ /** What a malformed auth signature fails the generate step with. */
25156
+ const INVALID_AUTH_SIGNATURE_MESSAGE = 'DepositFlow: the wallet returned a malformed authentication signature; expected 65 bytes of 0x-prefixed hex';
25157
+ /** What a malformed acceptance signature fails the confirm step with. */
25158
+ const INVALID_ACCEPTANCE_SIGNATURE_MESSAGE = 'DepositFlow: the wallet returned a malformed acceptance signature; expected 65 bytes of 0x-prefixed hex';
25159
+ /**
25160
+ * Is this an EIP-191 personal sign, in the only shape the protocol accepts?
25161
+ *
25162
+ * 65 bytes — `r`, `s`, `v` — 0x-prefixed. Nothing downstream checks it: the
25163
+ * ABI coder takes any even-length hex as `bytes`, so `'0x'` and a 64-byte
25164
+ * string both encode happily into a `depositData` the agreements service then
25165
+ * cannot verify against anything. The check has to happen where the signature
25166
+ * is taken, which is here.
25167
+ */
25168
+ function isPersonalSignature(signature) {
25169
+ return ethers.utils.isHexString(signature, 65);
25170
+ }
25171
+ const INITIAL$1 = {
25172
+ phase: 'idle',
25173
+ step: null,
25174
+ stepIndex: 0,
25175
+ stepTotal: 4,
25176
+ approvalRequired: true,
25177
+ contract: null,
25178
+ failure: null,
25179
+ };
25180
+ /** 1-based badge position, with `approve` dropped when it is out of scope. */
25181
+ function stepIndexOf(step, approvalRequired) {
25182
+ const order = approvalRequired
25183
+ ? ['generate', 'confirm', 'approve', 'request']
25184
+ : ['generate', 'confirm', 'request'];
25185
+ return order.indexOf(step) + 1;
25186
+ }
25187
+ function badgeFor(step, approvalRequired) {
25188
+ return {
25189
+ step,
25190
+ stepIndex: stepIndexOf(step, approvalRequired),
25191
+ stepTotal: approvalRequired ? 4 : 3,
25192
+ };
25193
+ }
25194
+ /**
25195
+ * The request step's extra outcomes, in the order the evidence supports.
25196
+ *
25197
+ * A revert is read for WHAT reverted before it is read as a shortfall.
25198
+ * `requestDepositWithKyc` reverts for a whole family of declared reasons —
25199
+ * `LendingPoolIsStopped`, `ClearingIsPending`, `UserNotKycd`, `UserBlocked`,
25200
+ * `UserNotInAllowList`, `InvalidTranche`, `BlockExpired` — and every one of
25201
+ * them used to arrive as `insufficient-balance`, which told a fully funded
25202
+ * lender to top up a wallet that was never short while their pool was simply
25203
+ * mid-clearing. So: a decoded protocol error is `reverted` and names itself.
25204
+ *
25205
+ * Only a token balance or allowance failure — or revert data nothing can
25206
+ * decode, on the `UNPREDICTABLE_GAS_LIMIT` ethers raises for a failed gas
25207
+ * estimate — is still `insufficient-balance`, which is the case that reason
25208
+ * was named for and the one where topping up is genuinely the answer.
25209
+ */
25210
+ function classifyRequest(err) {
25211
+ const revert = decodeRevert(err);
25212
+ if ((revert === null || revert === void 0 ? void 0 : revert.family) === 'protocol') {
25213
+ return {
25214
+ step: 'request',
25215
+ reason: 'reverted',
25216
+ revertError: revert.name,
25217
+ error: err,
25218
+ };
25219
+ }
25220
+ if (revert !== null || isUnpredictableGas(err)) {
25221
+ return { step: 'request', reason: 'insufficient-balance', error: err };
25222
+ }
25223
+ return classifyWalletFailure('request', err);
25224
+ }
25225
+ // ---------------------------------------------------------------------------
25226
+ // The flow
25227
+ // ---------------------------------------------------------------------------
25228
+ class DepositFlow extends Flow {
25229
+ constructor(_ports, opts) {
25230
+ var _a;
25231
+ super(INITIAL$1);
25232
+ this._ports = _ports;
25233
+ /**
25234
+ * The accept handshake. The run parks on this promise; `acceptContract`,
25235
+ * `declineContract` and `reset` each settle it with an `AcceptOutcome`.
25236
+ * Cleared the moment it settles so a stale resolver from an abandoned run
25237
+ * can never leak into the next one.
25238
+ */
25239
+ this._accept = null;
25240
+ /**
25241
+ * The run token that is between `acceptContract()` and the wallet
25242
+ * settling, or `null`.
25243
+ *
25244
+ * A token rather than a boolean, because the flag has to belong to the RUN
25245
+ * that set it: after `reset()` out of a wallet prompt that never answers,
25246
+ * the abandoned run's `finally` may not arrive for minutes, and a boolean
25247
+ * left standing refuses both Accept and Decline on every run after it.
25248
+ * A stale token simply is not the current generation.
25249
+ */
25250
+ this._acceptingFor = null;
25251
+ this._ttlMs = (_a = opts === null || opts === void 0 ? void 0 : opts.contractTtlMs) !== null && _a !== void 0 ? _a : CONTRACT_TTL_MS;
25252
+ this._defaultSpender = opts === null || opts === void 0 ? void 0 : opts.spender;
25253
+ // Called through the ports object, never captured off it: a consumer
25254
+ // whose clock is a method on its own object keeps its `this`.
25255
+ this._now = () => { var _a, _b; return (_b = (_a = _ports.now) === null || _a === void 0 ? void 0 : _a.call(_ports)) !== null && _b !== void 0 ? _b : Date.now(); };
25256
+ }
25257
+ /**
25258
+ * Sign the agreement and resume the parked run. A no-op when nothing is
25259
+ * parked, so a double tap cannot sign twice.
25260
+ */
25261
+ acceptContract() {
25262
+ return __awaiter(this, void 0, void 0, function* () {
25263
+ const bridge = this._accept;
25264
+ const contract = this._store.state.contract;
25265
+ if (!bridge || !contract || this._isAccepting())
25266
+ return;
25267
+ const token = this._store.generation;
25268
+ this._acceptingFor = token;
25269
+ this._store.patch(Object.assign({ phase: 'accepting-sign' }, badgeFor('confirm', this._store.state.approvalRequired)), token);
25270
+ try {
25271
+ const signature = yield this._ports.signMessage(contract.contractMessage);
25272
+ // Checked HERE, not at the encoder: this is the signature the
25273
+ // agreements service verifies the deposit against, and a wallet
25274
+ // that returned something other than 65 bytes has not signed
25275
+ // anything. Failing the confirm step names the step that produced
25276
+ // it; letting it through would put an unverifiable blob on chain.
25277
+ if (!isPersonalSignature(signature)) {
25278
+ bridge.settle({
25279
+ kind: 'failed',
25280
+ error: new Error(INVALID_ACCEPTANCE_SIGNATURE_MESSAGE),
25281
+ });
25282
+ return;
25283
+ }
25284
+ bridge.settle({ kind: 'accepted', signature });
25285
+ }
25286
+ catch (err) {
25287
+ // A WALLET error, and the only one this flow classifies as a
25288
+ // possible cancellation on the confirm step.
25289
+ bridge.settle({ kind: 'failed', error: err });
25290
+ }
25291
+ finally {
25292
+ // Only if this run still holds it: a `reset()` during the prompt
25293
+ // may have started another one, and that one's flag is its own.
25294
+ if (this._acceptingFor === token)
25295
+ this._acceptingFor = null;
25296
+ }
25297
+ });
25298
+ }
25299
+ /**
25300
+ * Back out of the agreement. The run ends on `declined` — a legitimate
25301
+ * choice, not a failure, and `state.failure` stays null.
25302
+ *
25303
+ * Ignored once `acceptContract()` has opened the wallet: an agreement in
25304
+ * the middle of being signed cannot also be refused. `reset()` is the way
25305
+ * out of a prompt that never answers.
25306
+ */
25307
+ declineContract() {
25308
+ const bridge = this._accept;
25309
+ if (!bridge || this._isAccepting())
25310
+ return;
25311
+ this._accept = null;
25312
+ bridge.settle({ kind: 'declined' });
25313
+ }
25314
+ /** True only while THIS generation is waiting on the acceptance signature. */
25315
+ _isAccepting() {
25316
+ return (this._acceptingFor !== null &&
25317
+ this._store.isCurrent(this._acceptingFor));
25318
+ }
25319
+ /** `reset()`: unpark the abandoned run and drop its handshake. */
25320
+ _onAbandon() {
25321
+ const bridge = this._accept;
25322
+ this._accept = null;
25323
+ this._acceptingFor = null;
25324
+ bridge === null || bridge === void 0 ? void 0 : bridge.settle({ kind: 'abandoned' });
25325
+ }
25326
+ // -----------------------------------------------------------------------
25327
+ _run(input, token) {
25328
+ return __awaiter(this, void 0, void 0, function* () {
25329
+ var _a;
25330
+ this._accept = null;
25331
+ this._acceptingFor = null;
25332
+ const ports = this._ports;
25333
+ const owner = input.userAddress.toLowerCase();
25334
+ // 0. The spender, from the input or from the chain config the facade
25335
+ // built this flow with. Without one there is nothing to read an
25336
+ // allowance against and nothing to approve — and guessing would
25337
+ // grant an approval to the wrong contract.
25338
+ const spender = (_a = input.spender) !== null && _a !== void 0 ? _a : this._defaultSpender;
25339
+ if (!spender) {
25340
+ this._fail(token, true, {
25341
+ step: 'generate',
25342
+ reason: 'failed',
25343
+ error: new Error(NO_SPENDER_MESSAGE),
25344
+ });
25345
+ return;
25346
+ }
25347
+ // 1. Allowance pre-check. Decides `approvalRequired` — and therefore
25348
+ // the badge total — before the lender is shown a single step. Read
25349
+ // live, never cached: an exact-amount approval is fully consumed by
25350
+ // the deposit it paid for, so a stale allowance is exactly the value
25351
+ // that would wrongly skip the approve and revert the deposit.
25352
+ let approvalRequired = true;
25353
+ try {
25354
+ const allowance = yield ports.readAllowance(owner, spender);
25355
+ approvalRequired = allowance.lt(input.amount);
25356
+ }
25357
+ catch (_b) {
25358
+ // A read failure is not a reason to skip an approval. Assume one is
25359
+ // needed: the cost is a redundant approve, the alternative is a
25360
+ // reverted deposit.
25361
+ approvalRequired = true;
25362
+ }
25363
+ if (!this._store.isCurrent(token))
25364
+ return;
25365
+ // 2. Generate — sign the auth message.
25366
+ this._store.patch(Object.assign({ approvalRequired, phase: 'generating-sign' }, badgeFor('generate', approvalRequired)), token);
25367
+ const timestamp = this._now();
25368
+ const signedMessage = buildAuthMessage(input, owner, timestamp);
25369
+ let signature;
25370
+ try {
25371
+ signature = yield ports.signMessage(signedMessage);
25372
+ }
25373
+ catch (err) {
25374
+ this._fail(token, approvalRequired, classifyWalletFailure('generate', err));
25375
+ return;
25376
+ }
25377
+ // The same EIP-191 shape as the acceptance, and the same reason to
25378
+ // check it: kasu-backend verifies this one to decide whether to issue
25379
+ // an agreement at all, so a malformed signature is a 4xx that says
25380
+ // nothing about which step produced it. Named here instead.
25381
+ if (!isPersonalSignature(signature)) {
25382
+ this._fail(token, approvalRequired, {
25383
+ step: 'generate',
25384
+ reason: 'failed',
25385
+ error: new Error(INVALID_AUTH_SIGNATURE_MESSAGE),
25386
+ });
25387
+ return;
25388
+ }
25389
+ if (!this._store.isCurrent(token))
25390
+ return;
25391
+ // 3. Generate — POST the request.
25392
+ this._store.patch(Object.assign({ phase: 'generating-fetch' }, badgeFor('generate', approvalRequired)), token);
25393
+ let contract;
25394
+ try {
25395
+ contract = yield ports.generateContract(Object.assign({ address: owner, signature,
25396
+ timestamp,
25397
+ signedMessage, poolId: input.poolId, trancheId: input.trancheId, fixedTermConfigId: input.fixedTermConfigId, depositAmount: input.depositAmount }, displayFieldsOf(input.contractMessage)));
25398
+ }
25399
+ catch (err) {
25400
+ // Never a wallet rejection: this step is an HTTP call, and the
25401
+ // lender's wallet was not involved in it. A backend that happened
25402
+ // to echo the words "user rejected" must not be reported to them as
25403
+ // something they did.
25404
+ this._fail(token, approvalRequired, {
25405
+ step: 'generate',
25406
+ reason: 'failed',
25407
+ error: err,
25408
+ });
25409
+ return;
25410
+ }
25411
+ if (!this._store.isCurrent(token))
25412
+ return;
25413
+ // 4. Park on the agreement until the consumer accepts or declines.
25414
+ const outcome = yield new Promise((resolve) => {
25415
+ this._accept = { settle: resolve };
25416
+ this._store.patch(Object.assign({ phase: 'awaiting-accept', contract }, badgeFor('confirm', approvalRequired)), token);
25417
+ });
25418
+ this._accept = null;
25419
+ // An abandoned run lands here too — `reset()` unparks it. The token
25420
+ // check is what tells the two apart.
25421
+ if (!this._store.isCurrent(token))
25422
+ return;
25423
+ if (outcome.kind === 'declined') {
25424
+ this._store.patch(Object.assign({ phase: 'declined' }, badgeFor('confirm', approvalRequired)), token);
25425
+ return;
25426
+ }
25427
+ if (outcome.kind !== 'accepted') {
25428
+ if (outcome.kind === 'failed') {
25429
+ this._fail(token, approvalRequired, classifyWalletFailure('confirm', outcome.error));
25430
+ }
25431
+ return;
25432
+ }
25433
+ // 5. TTL guard. Checked here because this is where the idling happens:
25434
+ // the lender has just spent as long as they wanted reading. An
25435
+ // expired agreement is refused rather than broadcast — the on-chain
25436
+ // call would revert, after a wallet prompt and a gas estimate, with
25437
+ // nothing on screen explaining why.
25438
+ if (this._now() > contract.timestamp + this._ttlMs) {
25439
+ this._fail(token, approvalRequired, {
25440
+ step: 'request',
25441
+ reason: 'contract-expired',
25442
+ });
25443
+ return;
25444
+ }
25445
+ // The acceptance signature is already known to be 65 bytes, but the
25446
+ // encoder also reads `timestamp`, `contractVersion` and
25447
+ // `contractType` off a response the SDK did not produce. `start()`
25448
+ // promises it does not reject, so anything that throws in here becomes
25449
+ // a failure the consumer can render rather than an unhandled rejection
25450
+ // in an application that was told it never had to catch one.
25451
+ let depositData;
25452
+ try {
25453
+ depositData = encodeDepositData({
25454
+ signature: outcome.signature,
25455
+ timestamp: contract.timestamp,
25456
+ contractVersion: contract.contractVersion,
25457
+ contractType: asContractType(contract.contractType),
25458
+ });
25459
+ }
25460
+ catch (err) {
25461
+ this._fail(token, approvalRequired, {
25462
+ step: 'request',
25463
+ reason: 'failed',
25464
+ error: err,
25465
+ });
25466
+ return;
25467
+ }
25468
+ // 6. Approve — the EXACT amount, never `MaxUint256`. House rule: an
25469
+ // unlimited allowance outlives the deposit it was granted for, and a
25470
+ // later exploit of the spender would drain a wallet that has long
25471
+ // since stopped lending.
25472
+ if (approvalRequired) {
25473
+ this._store.patch(Object.assign({ phase: 'approve' }, badgeFor('approve', approvalRequired)), token);
25474
+ try {
25475
+ const tx = yield ports.approve(spender, input.amount);
25476
+ yield tx.wait();
25477
+ }
25478
+ catch (err) {
25479
+ this._fail(token, approvalRequired, classifyWalletFailure('approve', err));
25480
+ return;
25481
+ }
25482
+ if (!this._store.isCurrent(token))
25483
+ return;
25484
+ }
25485
+ // 7. Request — KYC signature, then the deposit and its receipt.
25486
+ this._store.patch(Object.assign({ phase: 'request-sign' }, badgeFor('request', approvalRequired)), token);
25487
+ // The two KYC ports reach the consumer's own backend, so they fail the
25488
+ // way the generate step does: `failed`, with the error kept. Running
25489
+ // them through the rejection classifier would let a backend wording —
25490
+ // "Declined", "request rejected" — end a run as "you cancelled in your
25491
+ // wallet", with the real error discarded and nothing to report.
25492
+ let kyc;
25493
+ try {
25494
+ const kycParams = yield ports.buildKycParams(owner);
25495
+ kyc = yield ports.getKycSignature(kycParams);
25496
+ }
25497
+ catch (err) {
25498
+ this._fail(token, approvalRequired, {
25499
+ step: 'request',
25500
+ reason: 'failed',
25501
+ error: err,
25502
+ });
25503
+ return;
25504
+ }
25505
+ if (!this._store.isCurrent(token))
25506
+ return;
25507
+ try {
25508
+ const tx = yield ports.deposit({
25509
+ poolId: input.poolId,
25510
+ trancheId: input.trancheId,
25511
+ amount: input.amount,
25512
+ kycSignature: {
25513
+ blockExpiration: kyc.blockExpiration,
25514
+ signature: kyc.signature,
25515
+ },
25516
+ depositData,
25517
+ fixedTermConfigId: input.fixedTermConfigId,
25518
+ });
25519
+ this._store.patch(Object.assign({ phase: 'request-confirm' }, badgeFor('request', approvalRequired)), token);
25520
+ yield tx.wait();
25521
+ }
25522
+ catch (err) {
25523
+ this._fail(token, approvalRequired, classifyRequest(err));
25524
+ return;
25525
+ }
25526
+ if (!this._store.isCurrent(token))
25527
+ return;
25528
+ this._store.patch(Object.assign({ phase: 'success' }, badgeFor('request', approvalRequired)), token);
25529
+ });
25530
+ }
25531
+ _fail(token, approvalRequired, failure) {
25532
+ this._store.patch(Object.assign({ phase: 'error', failure }, badgeFor(failure.step, approvalRequired)), token);
25533
+ }
25534
+ }
25535
+ // ---------------------------------------------------------------------------
25536
+ // Message building
25537
+ // ---------------------------------------------------------------------------
25538
+ /**
25539
+ * The auth message, in whichever format the consumer asked for. Both builders
25540
+ * are the byte-exact protocol strings from `domain/loan-contract` — the string
25541
+ * signed here and the body posted from `_run` state the SAME timestamp and the
25542
+ * SAME lowercased address, because kasu-backend rebuilds one from the other.
25543
+ */
25544
+ function buildAuthMessage(input, owner, timestamp) {
25545
+ if (input.contractMessage.format === 'legacy') {
25546
+ return buildLegacyContractRequestMessage(owner, timestamp);
25547
+ }
25548
+ const { strategyName, region, optionName, amountLabel } = input.contractMessage;
25549
+ return buildLoanAgreementSignMessage({
25550
+ strategyName,
25551
+ region,
25552
+ optionName,
25553
+ amountLabel,
25554
+ timestamp,
25555
+ });
25556
+ }
25557
+ /**
25558
+ * The four display fields, present only on the new format. The backend picks
25559
+ * its verification path on their presence: all four → the human-readable
25560
+ * format, any missing → the legacy string.
25561
+ */
25562
+ function displayFieldsOf(message) {
25563
+ if (message.format === 'legacy')
25564
+ return {};
25565
+ return {
25566
+ strategyName: message.strategyName,
25567
+ region: message.region,
25568
+ optionName: message.optionName,
25569
+ amountLabel: message.amountLabel,
25570
+ };
25571
+ }
25572
+
25573
+ const INITIAL = {
25574
+ phase: 'idle',
25575
+ step: null,
25576
+ isMax: false,
25577
+ failure: null,
25578
+ };
25579
+ /** What a KYC pre-check with no `buildKycParams` to call fails with. */
25580
+ const NO_KYC_PARAMS_MESSAGE = 'WithdrawFlow: getKycSignature was supplied without buildKycParams; build the flow with kasu.flows.withdraw() or pass both';
25581
+ // ---------------------------------------------------------------------------
25582
+ // The flow
25583
+ // ---------------------------------------------------------------------------
25584
+ class WithdrawFlow extends Flow {
25585
+ constructor(_ports) {
25586
+ super(INITIAL);
25587
+ this._ports = _ports;
25588
+ }
25589
+ _run(input, token) {
25590
+ return __awaiter(this, void 0, void 0, function* () {
25591
+ const ports = this._ports;
25592
+ const isMax = input.amount === 'max';
25593
+ // 1. The optional KYC pre-check: build the params, exchange them for a
25594
+ // signature. Both ports reach the application's own backend, so a
25595
+ // throw here is ALWAYS `failed` — running it through the wallet
25596
+ // rejection classifier would let a backend's "Declined" be reported
25597
+ // to a lender as something they did, with the real error dropped.
25598
+ if (ports.getKycSignature) {
25599
+ this._store.patch({ phase: 'checking-kyc', step: 'kyc', isMax }, token);
25600
+ try {
25601
+ if (!ports.buildKycParams) {
25602
+ throw new Error(NO_KYC_PARAMS_MESSAGE);
25603
+ }
25604
+ const params = yield ports.buildKycParams(input.userAddress);
25605
+ yield ports.getKycSignature(params);
25606
+ }
25607
+ catch (err) {
25608
+ this._store.patch({
25609
+ phase: 'error',
25610
+ failure: { step: 'kyc', reason: 'failed', error: err },
25611
+ }, token);
25612
+ return;
25613
+ }
25614
+ if (!this._store.isCurrent(token))
25615
+ return;
25616
+ }
25617
+ // 2. The submission. This one IS a wallet call, so it keeps the
25618
+ // `cancelled` / `failed` split.
25619
+ this._store.patch({ phase: 'request-sign', step: 'request', isMax }, token);
25620
+ try {
25621
+ const tx = input.amount === 'max'
25622
+ ? yield ports.withdrawMax(input.poolId, input.trancheId, input.userAddress.toLowerCase())
25623
+ : yield ports.withdraw({
25624
+ poolId: input.poolId,
25625
+ trancheId: input.trancheId,
25626
+ amount: input.amount,
25627
+ });
25628
+ this._store.patch({ phase: 'request-confirm' }, token);
25629
+ yield tx.wait();
25630
+ }
25631
+ catch (err) {
25632
+ this._store.patch({
25633
+ phase: 'error',
25634
+ failure: classifyWalletFailure('request', err),
25635
+ }, token);
25636
+ return;
25637
+ }
25638
+ if (!this._store.isCurrent(token))
25639
+ return;
25640
+ this._store.patch({ phase: 'success' }, token);
25641
+ });
25642
+ }
25643
+ }
25644
+
25645
+ /**
25646
+ * Builds `DepositFlow` / `WithdrawFlow` instances wired to THIS Kasu instance:
25647
+ * its chain's stable token, its `LendingPoolManager`, its signer.
25648
+ *
25649
+ * ```ts
25650
+ * const flow = kasu.connect(signer).flows.deposit({
25651
+ * signMessage: (m) => signer.signMessage(m),
25652
+ * generateContract: (req) => postToMyProxy(req),
25653
+ * getKycSignature: (p) => postToMyBackend(p),
25654
+ * });
25655
+ * ```
25656
+ *
25657
+ * A flow built from a read-only instance constructs fine and reads fine — the
25658
+ * write ports throw `READ_ONLY_MESSAGE` when the run reaches them, exactly as
25659
+ * `kasu.deposits.deposit` does. Constructing is not the mistake; submitting is.
25660
+ *
25661
+ * It holds the `DepositsFacade` and NOTHING the facade already owns: no
25662
+ * `UserLending`, no chain id, no signer. Two paths to one behaviour is how the
25663
+ * KYC params a flow built came to differ from the ones `kasu.deposits`
25664
+ * built — the same class of drift the flows themselves exist to end.
25665
+ */
25666
+ class FlowsFacade {
25667
+ constructor(_deposits,
25668
+ /**
25669
+ * The chain's stable token, bound to whatever the Kasu instance holds.
25670
+ * A factory rather than a contract: it is one `new Contract`, and a
25671
+ * cached binding would outlive the config it was built from.
24413
25672
  */
24414
- withdraw(params) {
24415
- return __awaiter(this, void 0, void 0, function* () {
24416
- this.assertWritable();
24417
- return yield this._userLending.requestWithdrawalInAsset(params.poolId, params.trancheId, params.amount);
24418
- });
24419
- }
25673
+ _erc20,
25674
+ /** The read-only flag `Kasu` already computed — never re-derived here. */
25675
+ _isReadOnly,
24420
25676
  /**
24421
- * Withdraw the entire balance from a tranche.
25677
+ * The ERC-20 spender every deposit run approves: this chain's
25678
+ * `LendingPoolManager`, which is the only contract the default deposit
25679
+ * port calls. Passed to the flow so a consumer never has to hand-wire
25680
+ * an address whose only wrong value grants an approval to the wrong
25681
+ * contract.
24422
25682
  */
24423
- withdrawMax(poolId, trancheId, userAddress) {
24424
- return __awaiter(this, void 0, void 0, function* () {
24425
- this.assertWritable();
24426
- return yield this._userLending.requestWithdrawalMax(poolId, trancheId, userAddress);
24427
- });
25683
+ _spender) {
25684
+ this._deposits = _deposits;
25685
+ this._erc20 = _erc20;
25686
+ this._isReadOnly = _isReadOnly;
25687
+ this._spender = _spender;
24428
25688
  }
24429
25689
  /**
24430
- * Build KYC signature parameters for the Nexera verification flow.
25690
+ * A deposit pipeline. `readAllowance`, `approve`, `deposit` and
25691
+ * `buildKycParams` default to the SDK's own implementations; pass any of
25692
+ * them to override (kasu-ui approves through its sponsored-gas path, for
25693
+ * one).
24431
25694
  *
24432
- * The integrator is responsible for sending these to their backend,
24433
- * which obtains the signature from Nexera's signing service.
24434
- */
24435
- buildKycParams(userAddress) {
24436
- return this._userLending.buildKycSignatureParams(userAddress, this._chainId);
24437
- }
24438
- /**
24439
- * Check whether the pool is currently in a clearing period
24440
- * (deposits/withdrawals are temporarily paused).
25695
+ * Each default is applied per key with `??`, not by spreading `ports` over
25696
+ * them: `{ ...defaults, ...ports }` lets an EXPLICITLY undefined value
25697
+ * delete the default it was meant to keep, and
25698
+ * `approve: sponsoredGas ? sponsoredApprove : undefined` is exactly how a
25699
+ * consumer writes a conditional override.
24441
25700
  */
24442
- isClearingPending(poolId) {
24443
- return __awaiter(this, void 0, void 0, function* () {
24444
- return yield this._userLending.isClearingPending(poolId);
25701
+ deposit(ports, opts) {
25702
+ var _a, _b, _c, _d, _e;
25703
+ return new DepositFlow({
25704
+ signMessage: ports.signMessage,
25705
+ generateContract: ports.generateContract,
25706
+ getKycSignature: ports.getKycSignature,
25707
+ buildKycParams: (_a = ports.buildKycParams) !== null && _a !== void 0 ? _a : ((userAddress) => this._deposits.buildKycParams(userAddress)),
25708
+ readAllowance: (_b = ports.readAllowance) !== null && _b !== void 0 ? _b : ((owner, spender) => this._erc20().allowance(owner, spender)),
25709
+ approve: (_c = ports.approve) !== null && _c !== void 0 ? _c : ((spender, amount) => __awaiter(this, void 0, void 0, function* () {
25710
+ this._assertWritable();
25711
+ // The EXACT amount the flow asked for. Nothing here
25712
+ // rounds it up, and nothing here substitutes
25713
+ // MaxUint256.
25714
+ return yield this._erc20().approve(spender, amount);
25715
+ })),
25716
+ deposit: (_d = ports.deposit) !== null && _d !== void 0 ? _d : ((params) => this._deposits.deposit(params)),
25717
+ now: ports.now,
25718
+ }, {
25719
+ contractTtlMs: opts === null || opts === void 0 ? void 0 : opts.contractTtlMs,
25720
+ // Per key here too, for the same reason the ports are: an
25721
+ // explicit `spender: undefined` must not delete the default.
25722
+ spender: (_e = opts === null || opts === void 0 ? void 0 : opts.spender) !== null && _e !== void 0 ? _e : this._spender,
24445
25723
  });
24446
25724
  }
24447
25725
  /**
24448
- * Get the current epoch number.
25726
+ * A withdrawal pipeline. Both write ports and `buildKycParams` default to
25727
+ * this instance; supplying `getKycSignature` turns the KYC pre-check on.
25728
+ * Defaults are applied per key, for the reason `deposit()` gives.
24449
25729
  */
24450
- getCurrentEpoch() {
24451
- return __awaiter(this, void 0, void 0, function* () {
24452
- return yield this._userLending.getCurrentEpoch();
25730
+ withdraw(ports = {}) {
25731
+ var _a, _b, _c;
25732
+ return new WithdrawFlow({
25733
+ buildKycParams: (_a = ports.buildKycParams) !== null && _a !== void 0 ? _a : ((userAddress) => this._deposits.buildKycParams(userAddress)),
25734
+ getKycSignature: ports.getKycSignature,
25735
+ withdraw: (_b = ports.withdraw) !== null && _b !== void 0 ? _b : ((params) => this._deposits.withdraw({
25736
+ poolId: params.poolId,
25737
+ trancheId: params.trancheId,
25738
+ amount: params.amount,
25739
+ })),
25740
+ withdrawMax: (_c = ports.withdrawMax) !== null && _c !== void 0 ? _c : ((poolId, trancheId, userAddress) => this._deposits.withdrawMax(poolId, trancheId, userAddress)),
24453
25741
  });
24454
25742
  }
25743
+ _assertWritable() {
25744
+ if (this._isReadOnly) {
25745
+ throw new Error(READ_ONLY_MESSAGE);
25746
+ }
25747
+ }
24455
25748
  }
24456
25749
 
24457
25750
  /**
@@ -24910,6 +26203,16 @@ function firstSubmissionTimestamp(events, fallback) {
24910
26203
  return fallback;
24911
26204
  return submissions.reduce((min, e) => (e.timestamp < min ? e.timestamp : min), Infinity);
24912
26205
  }
26206
+ /**
26207
+ * The LATEST event timestamp, or `fallback` when the timeline is empty — or
26208
+ * when every event predates it, because `fallback` seeds the reduction. That
26209
+ * seeding is deliberate and is kasu-ui's behaviour: `request.timestamp` is a
26210
+ * fact about the request, and an event indexed with an earlier clock must not
26211
+ * make the row look older than the request itself.
26212
+ */
26213
+ function lastEventTimestamp(events, fallback) {
26214
+ return events.reduce((max, e) => (e.timestamp > max ? e.timestamp : max), fallback);
26215
+ }
24913
26216
  /**
24914
26217
  * Has THIS request's cycle closed? — the single open/closed signal behind the
24915
26218
  * status vocabulary and behind Cancel.
@@ -24993,10 +26296,10 @@ const numOrNull = (str) => {
24993
26296
  * status would strand it.
24994
26297
  *
24995
26298
  * 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 —
26299
+ * it; the tranche display rename (`getTrancheDisplayName` on `trancheName`
26300
+ * and on `reallocationTargetTrancheName`, both of which are RAW here); the
26301
+ * pool-name split; the amount format. The "view loan agreement" affordance is
26302
+ * a fact, and it follows from two fields already here —
25000
26303
  * `requestType === 'Deposit' && statusCode !== 'cancelled' && statusCode !== 'rejected'`
25001
26304
  * — because neither a cancelled nor a fully-rejected deposit ever issued one,
25002
26305
  * and withdrawals sign no agreement at all.
@@ -25031,6 +26334,12 @@ function deriveRequestState(request) {
25031
26334
  initiatedAmount: initiated
25032
26335
  ? initiatedAmountOf(request.events)
25033
26336
  : null,
26337
+ rejectedAmount: rejected,
26338
+ reallocatedOutAmount: reallocation ? num(reallocation.assetAmount) : 0,
26339
+ reallocationTargetTrancheName: reallocation
26340
+ ? reallocation.trancheName
26341
+ : null,
26342
+ lastTimestamp: lastEventTimestamp(request.events, request.timestamp),
25034
26343
  submissionCount: countSubmissions(request.events),
25035
26344
  // `firstSubmissionTimestamp` needs a fallback it will never use here:
25036
26345
  // the bundle is non-empty on every path that reaches the call.
@@ -25150,8 +26459,9 @@ class PortfolioFacade {
25150
26459
  /**
25151
26460
  * High-level entry point for external integrators.
25152
26461
  *
25153
- * Provides three domain facades — `strategies`, `deposits`, `portfolio` — and
25154
- * exposes the underlying `KasuSdk` services via `.services` for power-users.
26462
+ * Provides four domain facades — `strategies`, `deposits`, `portfolio`,
26463
+ * `flows` — and exposes the underlying `KasuSdk` services via `.services` for
26464
+ * power-users.
25155
26465
  *
25156
26466
  * ```ts
25157
26467
  * import { Kasu } from '@kasufinance/kasu-sdk';
@@ -25175,8 +26485,13 @@ class Kasu {
25175
26485
  this._signerOrProvider = signerOrProvider;
25176
26486
  this._configOverrides = configOverrides;
25177
26487
  this.strategies = new StrategiesFacade(sdk.DataService, sdk.UserLending);
25178
- this.deposits = new DepositsFacade(sdk.UserLending, chainConfig.chainId.toString(), !ethers.Signer.isSigner(signerOrProvider));
26488
+ // Derived ONCE and handed to every facade that needs it. Each of them
26489
+ // re-deriving `Signer.isSigner` is how two facades come to disagree
26490
+ // about whether the same instance can write.
26491
+ const isReadOnly = !ethers.Signer.isSigner(signerOrProvider);
26492
+ this.deposits = new DepositsFacade(sdk.UserLending, chainConfig.chainId.toString(), isReadOnly);
25179
26493
  this.portfolio = new PortfolioFacade(sdk.DataService, sdk.UserLending, sdk.Portfolio);
26494
+ this.flows = new FlowsFacade(this.deposits, () => erc20Of(chainConfig, signerOrProvider), isReadOnly, chainConfig.contracts.LendingPoolManager);
25180
26495
  }
25181
26496
  /**
25182
26497
  * Create a Kasu instance with built-in chain config.
@@ -25308,6 +26623,23 @@ function rpcUrlsOf(chainConfig) {
25308
26623
  var _a;
25309
26624
  return (_a = chainConfig.rpcUrls) !== null && _a !== void 0 ? _a : [];
25310
26625
  }
26626
+ /**
26627
+ * The chain's stable token, bound to whatever this instance holds.
26628
+ *
26629
+ * Built per call rather than cached: it is one `new Contract`, and a cached
26630
+ * binding would outlive the signer a `connect()` replaced. A config with no
26631
+ * `stableAsset` (a hand-written entry from before 2.5.0) throws here rather
26632
+ * than inside ethers, naming the two ports that make the flow work without
26633
+ * one.
26634
+ */
26635
+ function erc20Of(chainConfig, signerOrProvider) {
26636
+ var _a;
26637
+ const address = (_a = stableAssetOf(chainConfig)) === null || _a === void 0 ? void 0 : _a.address;
26638
+ if (!address) {
26639
+ throw new Error('Kasu: this chain config has no stableAsset; pass readAllowance and approve ports explicitly');
26640
+ }
26641
+ return IERC20MetadataAbi__factory.connect(address, signerOrProvider);
26642
+ }
25311
26643
  /**
25312
26644
  * The read-only provider used when the caller passes no `signerOrProvider`.
25313
26645
  *
@@ -25674,165 +27006,6 @@ function getInstitutionalLender(originator) {
25674
27006
  return originator.name === APXIUM.name ? RIXON_CAPITAL : null;
25675
27007
  }
25676
27008
 
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
27009
  /**
25837
27010
  * Cycles and the clearing window — the protocol's weekly clock, as numbers.
25838
27011
  *
@@ -25979,69 +27152,20 @@ function getTrancheDisplayName(trancheName, pool) {
25979
27152
  : trancheName;
25980
27153
  }
25981
27154
 
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
-
26038
27155
  exports.APXIUM = APXIUM;
26039
27156
  exports.AU_ALPHA3 = AU_ALPHA3;
26040
27157
  exports.AU_MIN_CUMULATIVE_BY_STABLE = AU_MIN_CUMULATIVE_BY_STABLE;
26041
27158
  exports.CHAIN_CONFIGS = CHAIN_CONFIGS;
26042
27159
  exports.CLEARING_WINDOW_SECONDS = CLEARING_WINDOW_SECONDS;
27160
+ exports.CONTRACT_TTL_MS = CONTRACT_TTL_MS;
27161
+ exports.DepositFlow = DepositFlow;
26043
27162
  exports.DepositsFacade = DepositsFacade;
26044
27163
  exports.EPOCHS_IN_YEAR = EPOCHS_IN_YEAR;
27164
+ exports.Flow = Flow;
27165
+ exports.FlowStore = FlowStore;
27166
+ exports.FlowsFacade = FlowsFacade;
27167
+ exports.INVALID_ACCEPTANCE_SIGNATURE_MESSAGE = INVALID_ACCEPTANCE_SIGNATURE_MESSAGE;
27168
+ exports.INVALID_AUTH_SIGNATURE_MESSAGE = INVALID_AUTH_SIGNATURE_MESSAGE;
26045
27169
  exports.INVOICEMATE = INVOICEMATE;
26046
27170
  exports.Kasu = Kasu;
26047
27171
  exports.KasuSdk = KasuSdk;
@@ -26049,11 +27173,15 @@ exports.MAX_LENDING_AMOUNT_FALLBACK = MAX_LENDING_AMOUNT_FALLBACK;
26049
27173
  exports.MIN_LENDING_AMOUNT_FALLBACK = MIN_LENDING_AMOUNT_FALLBACK;
26050
27174
  exports.MIN_TRANCHE_CAPACITY = MIN_TRANCHE_CAPACITY;
26051
27175
  exports.NO_DIRECTUS_URL_MESSAGE = NO_DIRECTUS_URL_MESSAGE;
27176
+ exports.NO_KYC_PARAMS_MESSAGE = NO_KYC_PARAMS_MESSAGE;
27177
+ exports.NO_SPENDER_MESSAGE = NO_SPENDER_MESSAGE;
26052
27178
  exports.PortfolioFacade = PortfolioFacade;
27179
+ exports.READ_ONLY_MESSAGE = READ_ONLY_MESSAGE;
26053
27180
  exports.RIXON_CAPITAL = RIXON_CAPITAL;
26054
27181
  exports.SdkConfig = SdkConfig;
26055
27182
  exports.StrategiesFacade = StrategiesFacade;
26056
27183
  exports.UPPER_MEZZANINE = UPPER_MEZZANINE;
27184
+ exports.WithdrawFlow = WithdrawFlow;
26057
27185
  exports.apyToEpochRate = apyToEpochRate;
26058
27186
  exports.asContractType = asContractType;
26059
27187
  exports.auMinimumRemaining = auMinimumRemaining;
@@ -26063,14 +27191,17 @@ exports.buildFullNameRequestMessage = buildFullNameRequestMessage;
26063
27191
  exports.buildLegacyContractRequestMessage = buildLegacyContractRequestMessage;
26064
27192
  exports.buildLoanAgreementSignMessage = buildLoanAgreementSignMessage;
26065
27193
  exports.ceilToCents = ceilToCents;
27194
+ exports.classifyWalletFailure = classifyWalletFailure;
26066
27195
  exports.compareTrancheSeniority = compareTrancheSeniority;
26067
27196
  exports.computeSettlementWindow = computeSettlementWindow;
26068
27197
  exports.countSubmissions = countSubmissions;
27198
+ exports.decodeRevert = decodeRevert;
26069
27199
  exports.deriveCycleDates = deriveCycleDates;
26070
27200
  exports.derivePoolStatus = derivePoolStatus;
26071
27201
  exports.deriveRequestState = deriveRequestState;
26072
27202
  exports.encodeDepositData = encodeDepositData;
26073
27203
  exports.epochRateToApy = epochRateToApy;
27204
+ exports.extractRevertData = extractRevertData;
26074
27205
  exports.fetchUnusedPoolIds = fetchUnusedPoolIds;
26075
27206
  exports.firstSubmissionTimestamp = firstSubmissionTimestamp;
26076
27207
  exports.floorToCents = floorToCents;
@@ -26084,6 +27215,7 @@ exports.isBelowMinimumCapacity = isBelowMinimumCapacity;
26084
27215
  exports.isCycleClosed = isCycleClosed;
26085
27216
  exports.isUnpredictableGas = isUnpredictableGas;
26086
27217
  exports.isUserRejected = isUserRejected;
27218
+ exports.lastEventTimestamp = lastEventTimestamp;
26087
27219
  exports.maxNetRateCeiling = maxNetRateCeiling;
26088
27220
  exports.netEffectiveApy = netEffectiveApy;
26089
27221
  exports.netTrancheApyBounds = netTrancheApyBounds;