@kasufinance/kasu-sdk 2.5.0 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/README.md +144 -7
  2. package/dist/bundle.cjs.js +1667 -21
  3. package/dist/bundle.esm.js +1630 -22
  4. package/dist/domain/au-minimum.d.ts +135 -0
  5. package/dist/domain/au-minimum.js +154 -0
  6. package/dist/domain/au-minimum.js.map +1 -0
  7. package/dist/domain/index.d.ts +18 -3
  8. package/dist/domain/index.js +13 -3
  9. package/dist/domain/index.js.map +1 -1
  10. package/dist/domain/loan-contract.d.ts +174 -0
  11. package/dist/domain/loan-contract.js +160 -0
  12. package/dist/domain/loan-contract.js.map +1 -0
  13. package/dist/domain/requests.d.ts +219 -0
  14. package/dist/domain/requests.js +218 -0
  15. package/dist/domain/requests.js.map +1 -0
  16. package/dist/domain/settlement.d.ts +97 -0
  17. package/dist/domain/settlement.js +117 -0
  18. package/dist/domain/settlement.js.map +1 -0
  19. package/dist/domain/wallet-errors.d.ts +92 -0
  20. package/dist/domain/wallet-errors.js +155 -0
  21. package/dist/domain/wallet-errors.js.map +1 -0
  22. package/dist/facade/chain-configs.js +7 -1
  23. package/dist/facade/chain-configs.js.map +1 -1
  24. package/dist/facade/deposits.js +2 -1
  25. package/dist/facade/deposits.js.map +1 -1
  26. package/dist/facade/flows.d.ts +95 -0
  27. package/dist/facade/flows.js +116 -0
  28. package/dist/facade/flows.js.map +1 -0
  29. package/dist/facade/index.d.ts +3 -0
  30. package/dist/facade/index.js +3 -0
  31. package/dist/facade/index.js.map +1 -1
  32. package/dist/facade/kasu.d.ts +6 -2
  33. package/dist/facade/kasu.js +28 -3
  34. package/dist/facade/kasu.js.map +1 -1
  35. package/dist/facade/read-only.d.ts +12 -0
  36. package/dist/facade/read-only.js +13 -0
  37. package/dist/facade/read-only.js.map +1 -0
  38. package/dist/facade/user-portfolio.d.ts +18 -0
  39. package/dist/facade/user-portfolio.js +23 -0
  40. package/dist/facade/user-portfolio.js.map +1 -1
  41. package/dist/flows/deposit-flow.d.ts +301 -0
  42. package/dist/flows/deposit-flow.js +358 -0
  43. package/dist/flows/deposit-flow.js.map +1 -0
  44. package/dist/flows/flow.d.ts +72 -0
  45. package/dist/flows/flow.js +110 -0
  46. package/dist/flows/flow.js.map +1 -0
  47. package/dist/flows/index.d.ts +19 -0
  48. package/dist/flows/index.js +17 -0
  49. package/dist/flows/index.js.map +1 -0
  50. package/dist/flows/observable.d.ts +59 -0
  51. package/dist/flows/observable.js +81 -0
  52. package/dist/flows/observable.js.map +1 -0
  53. package/dist/flows/withdraw-flow.d.ts +99 -0
  54. package/dist/flows/withdraw-flow.js +83 -0
  55. package/dist/flows/withdraw-flow.js.map +1 -0
  56. package/dist/index.d.ts +2 -0
  57. package/dist/index.js +7 -0
  58. package/dist/index.js.map +1 -1
  59. package/dist/services/DataService/data-service.js +3 -11
  60. package/dist/services/DataService/data-service.js.map +1 -1
  61. package/dist/services/DataService/directus-client.d.ts +26 -0
  62. package/dist/services/DataService/directus-client.js +38 -0
  63. package/dist/services/DataService/directus-client.js.map +1 -0
  64. package/dist/services/UserLending/user-lending.js +11 -7
  65. package/dist/services/UserLending/user-lending.js.map +1 -1
  66. package/package.json +8 -2
  67. package/src/domain/au-minimum.ts +192 -0
  68. package/src/domain/index.ts +73 -3
  69. package/src/domain/loan-contract.ts +275 -0
  70. package/src/domain/requests.ts +467 -0
  71. package/src/domain/settlement.ts +161 -0
  72. package/src/domain/wallet-errors.ts +173 -0
  73. package/src/facade/chain-configs.ts +7 -1
  74. package/src/facade/deposits.ts +2 -3
  75. package/src/facade/flows.ts +172 -0
  76. package/src/facade/index.ts +8 -0
  77. package/src/facade/kasu.ts +43 -3
  78. package/src/facade/read-only.ts +13 -0
  79. package/src/facade/user-portfolio.ts +24 -0
  80. package/src/flows/deposit-flow.ts +775 -0
  81. package/src/flows/flow.ts +108 -0
  82. package/src/flows/index.ts +45 -0
  83. package/src/flows/observable.ts +97 -0
  84. package/src/flows/withdraw-flow.ts +210 -0
  85. package/src/index.ts +9 -0
  86. package/src/services/DataService/data-service.ts +7 -25
  87. package/src/services/DataService/directus-client.ts +54 -0
  88. package/src/services/UserLending/user-lending.ts +17 -21
  89. package/dist/domain/deposit-bounds.test.d.ts +0 -1
  90. package/dist/domain/deposit-bounds.test.js +0 -135
  91. package/dist/domain/deposit-bounds.test.js.map +0 -1
  92. package/dist/domain/partners.test.d.ts +0 -1
  93. package/dist/domain/partners.test.js +0 -53
  94. package/dist/domain/partners.test.js.map +0 -1
  95. package/dist/domain/pools.test.d.ts +0 -1
  96. package/dist/domain/pools.test.js +0 -184
  97. package/dist/domain/pools.test.js.map +0 -1
  98. package/dist/domain/rates.test.d.ts +0 -1
  99. package/dist/domain/rates.test.js +0 -181
  100. package/dist/domain/rates.test.js.map +0 -1
  101. package/dist/domain/tranche-display-name.test.d.ts +0 -1
  102. package/dist/domain/tranche-display-name.test.js +0 -58
  103. package/dist/domain/tranche-display-name.test.js.map +0 -1
  104. package/dist/domain/tranches.test.d.ts +0 -1
  105. package/dist/domain/tranches.test.js +0 -206
  106. package/dist/domain/tranches.test.js.map +0 -1
  107. package/dist/facade/config.test.d.ts +0 -1
  108. package/dist/facade/config.test.js +0 -216
  109. package/dist/facade/config.test.js.map +0 -1
  110. package/dist/facade/facade.test.d.ts +0 -1
  111. package/dist/facade/facade.test.js +0 -201
  112. package/dist/facade/facade.test.js.map +0 -1
  113. package/dist/services/Locking/calculate-apy.test.d.ts +0 -1
  114. package/dist/services/Locking/calculate-apy.test.js +0 -41
  115. package/dist/services/Locking/calculate-apy.test.js.map +0 -1
  116. package/dist/tests/sample.test.d.ts +0 -1
  117. package/dist/tests/sample.test.js +0 -59
  118. package/dist/tests/sample.test.js.map +0 -1
  119. package/src/domain/deposit-bounds.test.ts +0 -200
  120. package/src/domain/partners.test.ts +0 -83
  121. package/src/domain/pools.test.ts +0 -260
  122. package/src/domain/rates.test.ts +0 -254
  123. package/src/domain/tranche-display-name.test.ts +0 -96
  124. package/src/domain/tranches.test.ts +0 -300
  125. package/src/facade/config.test.ts +0 -265
  126. package/src/facade/facade.test.ts +0 -250
  127. package/src/services/Locking/calculate-apy.test.ts +0 -52
  128. package/src/tests/sample.test.ts +0 -63
@@ -15721,6 +15721,43 @@ function filterArray(array, id_in) {
15721
15721
  }
15722
15722
  }
15723
15723
 
15724
+ /**
15725
+ * The error a CMS-only call raises on a deployment configured without
15726
+ * Directus. Named so a caller can match on it rather than on the message.
15727
+ */
15728
+ const NO_DIRECTUS_URL_MESSAGE = 'Kasu: this call needs Directus, but the SDK was configured without a ' +
15729
+ '`directusUrl`. On-chain data (pools, tranches, positions, requests) ' +
15730
+ 'works without one; CMS content does not.';
15731
+ /**
15732
+ * Build the Directus client, or a stand-in that refuses clearly.
15733
+ *
15734
+ * `directusUrl` is documented optional, and most of the SDK genuinely does not
15735
+ * need it — pools, tranches, positions and request history all come from the
15736
+ * subgraph and the chain. But `createDirectus('')` throws `Invalid URL` inside
15737
+ * the constructor, so omitting the URL used to make the whole SDK
15738
+ * unconstructable rather than merely CMS-less.
15739
+ *
15740
+ * With no URL, the services skip Directus where they can degrade (pool
15741
+ * descriptions, images and Directus pool names simply do not appear, and the
15742
+ * raw subgraph names are used instead), and a call that exists ONLY to read
15743
+ * CMS content rejects with `NO_DIRECTUS_URL_MESSAGE` — a sentence that says
15744
+ * what to configure, rather than a `null` dereference thrown from inside a
15745
+ * vendor SDK.
15746
+ */
15747
+ function createDirectusClient(directusUrl) {
15748
+ if (directusUrl) {
15749
+ return le(directusUrl)
15750
+ .with(ne())
15751
+ .with(Zp());
15752
+ }
15753
+ const refuse = () => {
15754
+ throw new Error(NO_DIRECTUS_URL_MESSAGE);
15755
+ };
15756
+ // A stand-in, not a client: every entry point the services use goes
15757
+ // through `request`, so refusing there covers all of them.
15758
+ return { request: refuse };
15759
+ }
15760
+
15724
15761
  const getPoolOverviewQuery = (ids) => gql `
15725
15762
  query getAllPoolOverview($epochId: String!, $unusedPools: [String]!) {
15726
15763
  lendingPools(
@@ -15876,15 +15913,7 @@ class DataService {
15876
15913
  this._kasuConfig = _kasuConfig;
15877
15914
  this._externalTvlAbi = KasuPoolExternalTVLAbi__factory.connect(_kasuConfig.contracts.ExternalTVL, signerOrProvider);
15878
15915
  this._graph = new GraphQLClient(_kasuConfig.subgraphUrl);
15879
- if (_kasuConfig.directusUrl) {
15880
- this._directus = le(_kasuConfig.directusUrl)
15881
- .with(ne())
15882
- .with(Zp());
15883
- }
15884
- else {
15885
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
15886
- this._directus = null;
15887
- }
15916
+ this._directus = createDirectusClient(_kasuConfig.directusUrl);
15888
15917
  }
15889
15918
  getUrlFromFile(fileName) {
15890
15919
  return `${this._kasuConfig.directusUrl}assets/${fileName}`;
@@ -16178,7 +16207,6 @@ class DataService {
16178
16207
  return __awaiter(this, void 0, void 0, function* () {
16179
16208
  var _a;
16180
16209
  const poolDelegateProfileAndHistoryDirectus = (yield this._directus.request(bs('PoolDelegateProfileAndHistory', {
16181
- // @ts-ignore Directus' deep-fields typing is fragile across versions
16182
16210
  fields: ['*', { otherPools: ['*'] }],
16183
16211
  })));
16184
16212
  const [poolNames, directusPoolNames] = yield Promise.all([
@@ -17138,9 +17166,7 @@ class UserLending {
17138
17166
  this._clearingCoordinatorAbi = IClearingCoordinatorAbi__factory.connect(_kasuConfig.contracts.ClearingCoordinator, signerOrProvider);
17139
17167
  this._systemVariablesAbi = ISystemVariablesAbi__factory.connect(_kasuConfig.contracts.SystemVariables, signerOrProvider);
17140
17168
  this._dataService = new DataService(_kasuConfig, signerOrProvider);
17141
- this._directus = le(_kasuConfig.directusUrl)
17142
- .with(ne())
17143
- .with(Zp());
17169
+ this._directus = createDirectusClient(_kasuConfig.directusUrl);
17144
17170
  }
17145
17171
  getUserTotalPendingAndActiveDepositedAmount(user) {
17146
17172
  return __awaiter(this, void 0, void 0, function* () {
@@ -17258,9 +17284,14 @@ class UserLending {
17258
17284
  unusedPools: this._kasuConfig.UNUSED_LENDING_POOL_IDS,
17259
17285
  epochId,
17260
17286
  }),
17261
- this._directus.request(bs('PoolOverview', {
17262
- fields: ['id', 'poolName', 'subheading'],
17263
- })),
17287
+ // Directus supplies the display pool NAME only; without it the
17288
+ // raw subgraph name is used, which is what the fallback below
17289
+ // already does for a pool with no CMS entry.
17290
+ this._kasuConfig.directusUrl
17291
+ ? this._directus.request(bs('PoolOverview', {
17292
+ fields: ['id', 'poolName', 'subheading'],
17293
+ }))
17294
+ : [],
17264
17295
  ]);
17265
17296
  const retn = [];
17266
17297
  for (const userRequest of subgraphResult.userRequests) {
@@ -24295,7 +24326,13 @@ const CHAIN_CONFIGS = {
24295
24326
  ClearingCoordinator: '',
24296
24327
  ExternalTVL: '',
24297
24328
  },
24298
- subgraphUrl: 'https://api.goldsky.com/api/public/project_cmgzlpxm300765np2a19421om/subgraphs/kasu-plume/prod',
24329
+ // The frozen Plume history is indexed on the LEGACY Goldsky project,
24330
+ // not the one the live chains use: the same path under the current
24331
+ // project 404s. Verified 2026-09-05 — this URL answers
24332
+ // `{ lendingPools { id name } }` with the three Plume pools; the
24333
+ // current-project spelling returns HTTP 404. Note the `/gn` suffix,
24334
+ // which the current project's URLs do not carry.
24335
+ subgraphUrl: 'https://api.goldsky.com/api/public/project_cm9t3064xeuyn01tgctdo3c17/subgraphs/kasu-plume/prod/gn',
24299
24336
  directusUrl: 'https://kasu-finance.directus.app/',
24300
24337
  unusedPoolIds: [],
24301
24338
  poolMetadataMapping: undefined,
@@ -24315,6 +24352,19 @@ const CHAIN_CONFIGS = {
24315
24352
  },
24316
24353
  };
24317
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
+
24318
24368
  /**
24319
24369
  * High-level facade for deposit and withdrawal operations.
24320
24370
  *
@@ -24343,7 +24393,7 @@ class DepositsFacade {
24343
24393
  */
24344
24394
  assertWritable() {
24345
24395
  if (this._isReadOnly) {
24346
- throw new Error('Kasu: this instance is read-only; call kasu.connect(signer) first');
24396
+ throw new Error(READ_ONLY_MESSAGE);
24347
24397
  }
24348
24398
  }
24349
24399
  /**
@@ -24416,6 +24466,1024 @@ class DepositsFacade {
24416
24466
  }
24417
24467
  }
24418
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
+ * Wallet and RPC error predicates — pure functions over `unknown`.
24811
+ *
24812
+ * Every consumer has to tell three things apart when a write fails: the lender
24813
+ * changed their mind, the call would revert, and everything else. The first
24814
+ * two must never be reported as a failure the lender should retry or contact
24815
+ * support about, and each wallet spells them differently, so the shapes are
24816
+ * enumerated once here.
24817
+ *
24818
+ * Lifted from kasu-ui's `src/lib/web3/is-user-rejected.ts` and kasu-mobile's
24819
+ * `src/features/lending/lib/errors.ts` — and now the UNION of the two, so
24820
+ * neither app has to keep a wrapper on top of this one.
24821
+ */
24822
+ /**
24823
+ * Did the lender reject the request in their wallet?
24824
+ *
24825
+ * Providers surface a rejection in different shapes, and the union of them is
24826
+ * the point of this function existing once:
24827
+ * - MetaMask and most EIP-1193 wallets: `code: 4001`
24828
+ * - Coinbase Wallet and ethers v5: `code: 'ACTION_REJECTED'`
24829
+ * - viem: a `UserRejectedRequestError` name
24830
+ * - WalletConnect and some Privy paths: `Error('User rejected the request')`
24831
+ * - a provider error WRAPPED by another layer, carrying the real code and
24832
+ * message on a nested `error` — the shape Privy's embedded wallet
24833
+ * surfaces on Expo, where the outer object says nothing useful
24834
+ * - ethers' own `reason` field, which is often the only place the text lands
24835
+ * - a PLAIN OBJECT carrying `message`, which `String(err)` would read as
24836
+ * `"[object Object]"`
24837
+ *
24838
+ * Previously kasu-ui's implementation verbatim, with kasu-mobile keeping its
24839
+ * own superset on top. That is precisely the drift this layer exists to stop —
24840
+ * a rejection kasu-mobile recognised and kasu-ui did not was reported to the
24841
+ * same lender as a failure on one app and a cancellation on the other. The
24842
+ * union lives here; the mobile wrapper goes.
24843
+ *
24844
+ * ## The text is read for a SUBJECT, not for a keyword
24845
+ *
24846
+ * The words alone are not the signal. "Declined" and "request rejected" are
24847
+ * also what a rate limiter, a risk engine and a KYC decision say, and those
24848
+ * arrive wrapped in exactly the same envelope a wallet error does — ethers'
24849
+ * `SERVER_ERROR` around `-32603` carries the upstream body on a nested
24850
+ * `error.message`, so `{ error: { message: 'request rejected: rate limit
24851
+ * exceeded' } }` is an RPC refusing to serve us, not a lender refusing to
24852
+ * sign. Reporting one as the other tells a lender they cancelled something
24853
+ * they never saw, and DROPS the real error on the floor.
24854
+ *
24855
+ * So a rejection is a machine-readable wallet code, or a sentence that names
24856
+ * the party who did it: "user rejected", "declined by the user", "cancelled by
24857
+ * the wallet". A bare "declined" is not a rejection, whatever else is on the
24858
+ * envelope.
24859
+ *
24860
+ * Within that rule, being generous is the safe direction: calling a genuine
24861
+ * fault a cancellation costs a lender one retry, while calling a deliberate
24862
+ * rejection a failure sends them to support to report a bug that does not
24863
+ * exist.
24864
+ */
24865
+ function isUserRejected(err) {
24866
+ var _a;
24867
+ if (!err)
24868
+ return false;
24869
+ if (typeof err === 'object') {
24870
+ const e = err;
24871
+ if (isRejectionCode(e.code))
24872
+ return true;
24873
+ // A wrapped provider error: the outer layer's code is its own, the
24874
+ // inner one is the wallet's.
24875
+ if (isRejectionCode((_a = e.error) === null || _a === void 0 ? void 0 : _a.code))
24876
+ return true;
24877
+ }
24878
+ const lower = rejectionText(err).toLowerCase();
24879
+ return USER_REJECTION_PATTERNS.some((pattern) => pattern.test(lower));
24880
+ }
24881
+ function isRejectionCode(code) {
24882
+ return code === 4001 || code === 'ACTION_REJECTED';
24883
+ }
24884
+ /**
24885
+ * The wordings that name the wallet or the person at it.
24886
+ *
24887
+ * Each one carries a SUBJECT — the identifier a wallet library uses, or the
24888
+ * verb with the party who performed it. That is what separates a lender
24889
+ * pressing Reject from a server declining to answer, and it is why none of
24890
+ * these is a bare "declined" or "rejected".
24891
+ */
24892
+ const USER_REJECTION_PATTERNS = [
24893
+ // Machine-readable markers that only a wallet layer emits, arriving as
24894
+ // text because something in between stringified the error.
24895
+ /action_rejected/,
24896
+ /user_rejected/,
24897
+ /userrejectedrequest/,
24898
+ // "the user did it", in the orders the wallets write it.
24899
+ /user\s+(?:has\s+)?(?:rejected|denied|declined|refused|cancell?ed)/,
24900
+ /(?:rejected|denied|declined|refused|cancell?ed)\s+by\s+(?:the\s+)?(?:user|wallet|signer|owner)/,
24901
+ // The wallet as the subject, which is how some embedded wallets word it.
24902
+ /(?:wallet|signer)\s+(?:rejected|denied|declined|refused)/,
24903
+ ];
24904
+ /**
24905
+ * Every place a wallet might have put the words: the message, ethers' `reason`,
24906
+ * the error's `name` (viem puts the whole signal there), and a wrapped error's
24907
+ * own three. Joined rather than picked, because which one carries the text
24908
+ * depends on how many layers wrapped it.
24909
+ *
24910
+ * A separator is used rather than a bare space: two fields must not be able to
24911
+ * form a phrase across the join that neither of them said.
24912
+ */
24913
+ function rejectionText(err) {
24914
+ var _a, _b, _c;
24915
+ if (typeof err !== 'object' || err === null)
24916
+ return String(err);
24917
+ const e = err;
24918
+ return [
24919
+ e.message,
24920
+ e.reason,
24921
+ e.name,
24922
+ (_a = e.error) === null || _a === void 0 ? void 0 : _a.message,
24923
+ (_b = e.error) === null || _b === void 0 ? void 0 : _b.reason,
24924
+ (_c = e.error) === null || _c === void 0 ? void 0 : _c.name,
24925
+ ]
24926
+ .filter((part) => typeof part === 'string')
24927
+ .join(' | ');
24928
+ }
24929
+ /**
24930
+ * Did the wallet or RPC signal that the on-chain call would revert?
24931
+ *
24932
+ * ethers v5 raises `UNPREDICTABLE_GAS_LIMIT` when gas estimation reverts —
24933
+ * most often an underlying `transferFrom` failing on an insufficient balance
24934
+ * or allowance. Distinct from a rejection: nothing was refused by the lender,
24935
+ * the transaction simply cannot succeed as composed, so the caller should
24936
+ * re-check its preconditions rather than invite a retry.
24937
+ */
24938
+ function isUnpredictableGas(err) {
24939
+ if (!err || typeof err !== 'object')
24940
+ return false;
24941
+ return err.code === 'UNPREDICTABLE_GAS_LIMIT';
24942
+ }
24943
+ /**
24944
+ * The `cancelled` / `failed` split, for a WALLET call on any step of any flow.
24945
+ *
24946
+ * A lender who pressed Reject is not a fault. Reporting one as the other is
24947
+ * how a support queue fills with people who did exactly what they meant to.
24948
+ *
24949
+ * Generic in the step so both flows share one implementation — it was
24950
+ * duplicated byte-for-byte in each of them, which is the same drift this
24951
+ * layer exists to stop, one level up.
24952
+ *
24953
+ * Only pass it an error a WALLET produced. An HTTP port's throw is always a
24954
+ * `failed`: the lender's wallet was not involved in it, so a backend that
24955
+ * happens to word a refusal "declined" must never be shown to them as
24956
+ * something they did.
24957
+ */
24958
+ function classifyWalletFailure(step, err) {
24959
+ return isUserRejected(err)
24960
+ ? { step, reason: 'cancelled' }
24961
+ : { step, reason: 'failed', error: err };
24962
+ }
24963
+
24964
+ /** Generated agreements are valid for five minutes upstream. */
24965
+ const CONTRACT_TTL_MS = 5 * 60 * 1000;
24966
+ /** What a run without a spender, from either source, fails with. */
24967
+ const NO_SPENDER_MESSAGE = 'DepositFlow: no ERC-20 spender; build the flow with kasu.flows.deposit() or pass `spender` on the input';
24968
+ const INITIAL$1 = {
24969
+ phase: 'idle',
24970
+ step: null,
24971
+ stepIndex: 0,
24972
+ stepTotal: 4,
24973
+ approvalRequired: true,
24974
+ contract: null,
24975
+ failure: null,
24976
+ };
24977
+ /** 1-based badge position, with `approve` dropped when it is out of scope. */
24978
+ function stepIndexOf(step, approvalRequired) {
24979
+ const order = approvalRequired
24980
+ ? ['generate', 'confirm', 'approve', 'request']
24981
+ : ['generate', 'confirm', 'request'];
24982
+ return order.indexOf(step) + 1;
24983
+ }
24984
+ function badgeFor(step, approvalRequired) {
24985
+ return {
24986
+ step,
24987
+ stepIndex: stepIndexOf(step, approvalRequired),
24988
+ stepTotal: approvalRequired ? 4 : 3,
24989
+ };
24990
+ }
24991
+ /**
24992
+ * The request step has a third outcome. `UNPREDICTABLE_GAS_LIMIT` here is
24993
+ * almost always `transferFrom` reverting on a balance that cannot cover the
24994
+ * deposit, and it takes precedence: nothing was refused by the lender, so
24995
+ * inviting a retry would just reproduce it.
24996
+ */
24997
+ function classifyRequest(err) {
24998
+ if (isUnpredictableGas(err)) {
24999
+ return { step: 'request', reason: 'insufficient-balance', error: err };
25000
+ }
25001
+ return classifyWalletFailure('request', err);
25002
+ }
25003
+ // ---------------------------------------------------------------------------
25004
+ // The flow
25005
+ // ---------------------------------------------------------------------------
25006
+ class DepositFlow extends Flow {
25007
+ constructor(_ports, opts) {
25008
+ var _a;
25009
+ super(INITIAL$1);
25010
+ this._ports = _ports;
25011
+ /**
25012
+ * The accept handshake. The run parks on this promise; `acceptContract`,
25013
+ * `declineContract` and `reset` each settle it with an `AcceptOutcome`.
25014
+ * Cleared the moment it settles so a stale resolver from an abandoned run
25015
+ * can never leak into the next one.
25016
+ */
25017
+ this._accept = null;
25018
+ /**
25019
+ * The run token that is between `acceptContract()` and the wallet
25020
+ * settling, or `null`.
25021
+ *
25022
+ * A token rather than a boolean, because the flag has to belong to the RUN
25023
+ * that set it: after `reset()` out of a wallet prompt that never answers,
25024
+ * the abandoned run's `finally` may not arrive for minutes, and a boolean
25025
+ * left standing refuses both Accept and Decline on every run after it.
25026
+ * A stale token simply is not the current generation.
25027
+ */
25028
+ this._acceptingFor = null;
25029
+ this._ttlMs = (_a = opts === null || opts === void 0 ? void 0 : opts.contractTtlMs) !== null && _a !== void 0 ? _a : CONTRACT_TTL_MS;
25030
+ this._defaultSpender = opts === null || opts === void 0 ? void 0 : opts.spender;
25031
+ // Called through the ports object, never captured off it: a consumer
25032
+ // whose clock is a method on its own object keeps its `this`.
25033
+ 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(); };
25034
+ }
25035
+ /**
25036
+ * Sign the agreement and resume the parked run. A no-op when nothing is
25037
+ * parked, so a double tap cannot sign twice.
25038
+ */
25039
+ acceptContract() {
25040
+ return __awaiter(this, void 0, void 0, function* () {
25041
+ const bridge = this._accept;
25042
+ const contract = this._store.state.contract;
25043
+ if (!bridge || !contract || this._isAccepting())
25044
+ return;
25045
+ const token = this._store.generation;
25046
+ this._acceptingFor = token;
25047
+ this._store.patch(Object.assign({ phase: 'accepting-sign' }, badgeFor('confirm', this._store.state.approvalRequired)), token);
25048
+ try {
25049
+ const signature = yield this._ports.signMessage(contract.contractMessage);
25050
+ bridge.settle({ kind: 'accepted', signature });
25051
+ }
25052
+ catch (err) {
25053
+ // A WALLET error, and the only one this flow classifies as a
25054
+ // possible cancellation on the confirm step.
25055
+ bridge.settle({ kind: 'failed', error: err });
25056
+ }
25057
+ finally {
25058
+ // Only if this run still holds it: a `reset()` during the prompt
25059
+ // may have started another one, and that one's flag is its own.
25060
+ if (this._acceptingFor === token)
25061
+ this._acceptingFor = null;
25062
+ }
25063
+ });
25064
+ }
25065
+ /**
25066
+ * Back out of the agreement. The run ends on `declined` — a legitimate
25067
+ * choice, not a failure, and `state.failure` stays null.
25068
+ *
25069
+ * Ignored once `acceptContract()` has opened the wallet: an agreement in
25070
+ * the middle of being signed cannot also be refused. `reset()` is the way
25071
+ * out of a prompt that never answers.
25072
+ */
25073
+ declineContract() {
25074
+ const bridge = this._accept;
25075
+ if (!bridge || this._isAccepting())
25076
+ return;
25077
+ this._accept = null;
25078
+ bridge.settle({ kind: 'declined' });
25079
+ }
25080
+ /** True only while THIS generation is waiting on the acceptance signature. */
25081
+ _isAccepting() {
25082
+ return (this._acceptingFor !== null &&
25083
+ this._store.isCurrent(this._acceptingFor));
25084
+ }
25085
+ /** `reset()`: unpark the abandoned run and drop its handshake. */
25086
+ _onAbandon() {
25087
+ const bridge = this._accept;
25088
+ this._accept = null;
25089
+ this._acceptingFor = null;
25090
+ bridge === null || bridge === void 0 ? void 0 : bridge.settle({ kind: 'abandoned' });
25091
+ }
25092
+ // -----------------------------------------------------------------------
25093
+ _run(input, token) {
25094
+ return __awaiter(this, void 0, void 0, function* () {
25095
+ var _a;
25096
+ this._accept = null;
25097
+ this._acceptingFor = null;
25098
+ const ports = this._ports;
25099
+ const owner = input.userAddress.toLowerCase();
25100
+ // 0. The spender, from the input or from the chain config the facade
25101
+ // built this flow with. Without one there is nothing to read an
25102
+ // allowance against and nothing to approve — and guessing would
25103
+ // grant an approval to the wrong contract.
25104
+ const spender = (_a = input.spender) !== null && _a !== void 0 ? _a : this._defaultSpender;
25105
+ if (!spender) {
25106
+ this._fail(token, true, {
25107
+ step: 'generate',
25108
+ reason: 'failed',
25109
+ error: new Error(NO_SPENDER_MESSAGE),
25110
+ });
25111
+ return;
25112
+ }
25113
+ // 1. Allowance pre-check. Decides `approvalRequired` — and therefore
25114
+ // the badge total — before the lender is shown a single step. Read
25115
+ // live, never cached: an exact-amount approval is fully consumed by
25116
+ // the deposit it paid for, so a stale allowance is exactly the value
25117
+ // that would wrongly skip the approve and revert the deposit.
25118
+ let approvalRequired = true;
25119
+ try {
25120
+ const allowance = yield ports.readAllowance(owner, spender);
25121
+ approvalRequired = allowance.lt(input.amount);
25122
+ }
25123
+ catch (_b) {
25124
+ // A read failure is not a reason to skip an approval. Assume one is
25125
+ // needed: the cost is a redundant approve, the alternative is a
25126
+ // reverted deposit.
25127
+ approvalRequired = true;
25128
+ }
25129
+ if (!this._store.isCurrent(token))
25130
+ return;
25131
+ // 2. Generate — sign the auth message.
25132
+ this._store.patch(Object.assign({ approvalRequired, phase: 'generating-sign' }, badgeFor('generate', approvalRequired)), token);
25133
+ const timestamp = this._now();
25134
+ const signedMessage = buildAuthMessage(input, owner, timestamp);
25135
+ let signature;
25136
+ try {
25137
+ signature = yield ports.signMessage(signedMessage);
25138
+ }
25139
+ catch (err) {
25140
+ this._fail(token, approvalRequired, classifyWalletFailure('generate', err));
25141
+ return;
25142
+ }
25143
+ if (!this._store.isCurrent(token))
25144
+ return;
25145
+ // 3. Generate — POST the request.
25146
+ this._store.patch(Object.assign({ phase: 'generating-fetch' }, badgeFor('generate', approvalRequired)), token);
25147
+ let contract;
25148
+ try {
25149
+ contract = yield ports.generateContract(Object.assign({ address: owner, signature,
25150
+ timestamp,
25151
+ signedMessage, poolId: input.poolId, trancheId: input.trancheId, fixedTermConfigId: input.fixedTermConfigId, depositAmount: input.depositAmount }, displayFieldsOf(input.contractMessage)));
25152
+ }
25153
+ catch (err) {
25154
+ // Never a wallet rejection: this step is an HTTP call, and the
25155
+ // lender's wallet was not involved in it. A backend that happened
25156
+ // to echo the words "user rejected" must not be reported to them as
25157
+ // something they did.
25158
+ this._fail(token, approvalRequired, {
25159
+ step: 'generate',
25160
+ reason: 'failed',
25161
+ error: err,
25162
+ });
25163
+ return;
25164
+ }
25165
+ if (!this._store.isCurrent(token))
25166
+ return;
25167
+ // 4. Park on the agreement until the consumer accepts or declines.
25168
+ const outcome = yield new Promise((resolve) => {
25169
+ this._accept = { settle: resolve };
25170
+ this._store.patch(Object.assign({ phase: 'awaiting-accept', contract }, badgeFor('confirm', approvalRequired)), token);
25171
+ });
25172
+ this._accept = null;
25173
+ // An abandoned run lands here too — `reset()` unparks it. The token
25174
+ // check is what tells the two apart.
25175
+ if (!this._store.isCurrent(token))
25176
+ return;
25177
+ if (outcome.kind === 'declined') {
25178
+ this._store.patch(Object.assign({ phase: 'declined' }, badgeFor('confirm', approvalRequired)), token);
25179
+ return;
25180
+ }
25181
+ if (outcome.kind !== 'accepted') {
25182
+ if (outcome.kind === 'failed') {
25183
+ this._fail(token, approvalRequired, classifyWalletFailure('confirm', outcome.error));
25184
+ }
25185
+ return;
25186
+ }
25187
+ // 5. TTL guard. Checked here because this is where the idling happens:
25188
+ // the lender has just spent as long as they wanted reading. An
25189
+ // expired agreement is refused rather than broadcast — the on-chain
25190
+ // call would revert, after a wallet prompt and a gas estimate, with
25191
+ // nothing on screen explaining why.
25192
+ if (this._now() > contract.timestamp + this._ttlMs) {
25193
+ this._fail(token, approvalRequired, {
25194
+ step: 'request',
25195
+ reason: 'contract-expired',
25196
+ });
25197
+ return;
25198
+ }
25199
+ const depositData = encodeDepositData({
25200
+ signature: outcome.signature,
25201
+ timestamp: contract.timestamp,
25202
+ contractVersion: contract.contractVersion,
25203
+ contractType: asContractType(contract.contractType),
25204
+ });
25205
+ // 6. Approve — the EXACT amount, never `MaxUint256`. House rule: an
25206
+ // unlimited allowance outlives the deposit it was granted for, and a
25207
+ // later exploit of the spender would drain a wallet that has long
25208
+ // since stopped lending.
25209
+ if (approvalRequired) {
25210
+ this._store.patch(Object.assign({ phase: 'approve' }, badgeFor('approve', approvalRequired)), token);
25211
+ try {
25212
+ const tx = yield ports.approve(spender, input.amount);
25213
+ yield tx.wait();
25214
+ }
25215
+ catch (err) {
25216
+ this._fail(token, approvalRequired, classifyWalletFailure('approve', err));
25217
+ return;
25218
+ }
25219
+ if (!this._store.isCurrent(token))
25220
+ return;
25221
+ }
25222
+ // 7. Request — KYC signature, then the deposit and its receipt.
25223
+ this._store.patch(Object.assign({ phase: 'request-sign' }, badgeFor('request', approvalRequired)), token);
25224
+ // The two KYC ports reach the consumer's own backend, so they fail the
25225
+ // way the generate step does: `failed`, with the error kept. Running
25226
+ // them through the rejection classifier would let a backend wording —
25227
+ // "Declined", "request rejected" — end a run as "you cancelled in your
25228
+ // wallet", with the real error discarded and nothing to report.
25229
+ let kyc;
25230
+ try {
25231
+ const kycParams = yield ports.buildKycParams(owner);
25232
+ kyc = yield ports.getKycSignature(kycParams);
25233
+ }
25234
+ catch (err) {
25235
+ this._fail(token, approvalRequired, {
25236
+ step: 'request',
25237
+ reason: 'failed',
25238
+ error: err,
25239
+ });
25240
+ return;
25241
+ }
25242
+ if (!this._store.isCurrent(token))
25243
+ return;
25244
+ try {
25245
+ const tx = yield ports.deposit({
25246
+ poolId: input.poolId,
25247
+ trancheId: input.trancheId,
25248
+ amount: input.amount,
25249
+ kycSignature: {
25250
+ blockExpiration: kyc.blockExpiration,
25251
+ signature: kyc.signature,
25252
+ },
25253
+ depositData,
25254
+ fixedTermConfigId: input.fixedTermConfigId,
25255
+ });
25256
+ this._store.patch(Object.assign({ phase: 'request-confirm' }, badgeFor('request', approvalRequired)), token);
25257
+ yield tx.wait();
25258
+ }
25259
+ catch (err) {
25260
+ this._fail(token, approvalRequired, classifyRequest(err));
25261
+ return;
25262
+ }
25263
+ if (!this._store.isCurrent(token))
25264
+ return;
25265
+ this._store.patch(Object.assign({ phase: 'success' }, badgeFor('request', approvalRequired)), token);
25266
+ });
25267
+ }
25268
+ _fail(token, approvalRequired, failure) {
25269
+ this._store.patch(Object.assign({ phase: 'error', failure }, badgeFor(failure.step, approvalRequired)), token);
25270
+ }
25271
+ }
25272
+ // ---------------------------------------------------------------------------
25273
+ // Message building
25274
+ // ---------------------------------------------------------------------------
25275
+ /**
25276
+ * The auth message, in whichever format the consumer asked for. Both builders
25277
+ * are the byte-exact protocol strings from `domain/loan-contract` — the string
25278
+ * signed here and the body posted from `_run` state the SAME timestamp and the
25279
+ * SAME lowercased address, because kasu-backend rebuilds one from the other.
25280
+ */
25281
+ function buildAuthMessage(input, owner, timestamp) {
25282
+ if (input.contractMessage.format === 'legacy') {
25283
+ return buildLegacyContractRequestMessage(owner, timestamp);
25284
+ }
25285
+ const { strategyName, region, optionName, amountLabel } = input.contractMessage;
25286
+ return buildLoanAgreementSignMessage({
25287
+ strategyName,
25288
+ region,
25289
+ optionName,
25290
+ amountLabel,
25291
+ timestamp,
25292
+ });
25293
+ }
25294
+ /**
25295
+ * The four display fields, present only on the new format. The backend picks
25296
+ * its verification path on their presence: all four → the human-readable
25297
+ * format, any missing → the legacy string.
25298
+ */
25299
+ function displayFieldsOf(message) {
25300
+ if (message.format === 'legacy')
25301
+ return {};
25302
+ return {
25303
+ strategyName: message.strategyName,
25304
+ region: message.region,
25305
+ optionName: message.optionName,
25306
+ amountLabel: message.amountLabel,
25307
+ };
25308
+ }
25309
+
25310
+ const INITIAL = {
25311
+ phase: 'idle',
25312
+ step: null,
25313
+ isMax: false,
25314
+ failure: null,
25315
+ };
25316
+ /** What a KYC pre-check with no `buildKycParams` to call fails with. */
25317
+ const NO_KYC_PARAMS_MESSAGE = 'WithdrawFlow: getKycSignature was supplied without buildKycParams; build the flow with kasu.flows.withdraw() or pass both';
25318
+ // ---------------------------------------------------------------------------
25319
+ // The flow
25320
+ // ---------------------------------------------------------------------------
25321
+ class WithdrawFlow extends Flow {
25322
+ constructor(_ports) {
25323
+ super(INITIAL);
25324
+ this._ports = _ports;
25325
+ }
25326
+ _run(input, token) {
25327
+ return __awaiter(this, void 0, void 0, function* () {
25328
+ const ports = this._ports;
25329
+ const isMax = input.amount === 'max';
25330
+ // 1. The optional KYC pre-check: build the params, exchange them for a
25331
+ // signature. Both ports reach the application's own backend, so a
25332
+ // throw here is ALWAYS `failed` — running it through the wallet
25333
+ // rejection classifier would let a backend's "Declined" be reported
25334
+ // to a lender as something they did, with the real error dropped.
25335
+ if (ports.getKycSignature) {
25336
+ this._store.patch({ phase: 'checking-kyc', step: 'kyc', isMax }, token);
25337
+ try {
25338
+ if (!ports.buildKycParams) {
25339
+ throw new Error(NO_KYC_PARAMS_MESSAGE);
25340
+ }
25341
+ const params = yield ports.buildKycParams(input.userAddress);
25342
+ yield ports.getKycSignature(params);
25343
+ }
25344
+ catch (err) {
25345
+ this._store.patch({
25346
+ phase: 'error',
25347
+ failure: { step: 'kyc', reason: 'failed', error: err },
25348
+ }, token);
25349
+ return;
25350
+ }
25351
+ if (!this._store.isCurrent(token))
25352
+ return;
25353
+ }
25354
+ // 2. The submission. This one IS a wallet call, so it keeps the
25355
+ // `cancelled` / `failed` split.
25356
+ this._store.patch({ phase: 'request-sign', step: 'request', isMax }, token);
25357
+ try {
25358
+ const tx = input.amount === 'max'
25359
+ ? yield ports.withdrawMax(input.poolId, input.trancheId, input.userAddress.toLowerCase())
25360
+ : yield ports.withdraw({
25361
+ poolId: input.poolId,
25362
+ trancheId: input.trancheId,
25363
+ amount: input.amount,
25364
+ });
25365
+ this._store.patch({ phase: 'request-confirm' }, token);
25366
+ yield tx.wait();
25367
+ }
25368
+ catch (err) {
25369
+ this._store.patch({
25370
+ phase: 'error',
25371
+ failure: classifyWalletFailure('request', err),
25372
+ }, token);
25373
+ return;
25374
+ }
25375
+ if (!this._store.isCurrent(token))
25376
+ return;
25377
+ this._store.patch({ phase: 'success' }, token);
25378
+ });
25379
+ }
25380
+ }
25381
+
25382
+ /**
25383
+ * Builds `DepositFlow` / `WithdrawFlow` instances wired to THIS Kasu instance:
25384
+ * its chain's stable token, its `LendingPoolManager`, its signer.
25385
+ *
25386
+ * ```ts
25387
+ * const flow = kasu.connect(signer).flows.deposit({
25388
+ * signMessage: (m) => signer.signMessage(m),
25389
+ * generateContract: (req) => postToMyProxy(req),
25390
+ * getKycSignature: (p) => postToMyBackend(p),
25391
+ * });
25392
+ * ```
25393
+ *
25394
+ * A flow built from a read-only instance constructs fine and reads fine — the
25395
+ * write ports throw `READ_ONLY_MESSAGE` when the run reaches them, exactly as
25396
+ * `kasu.deposits.deposit` does. Constructing is not the mistake; submitting is.
25397
+ *
25398
+ * It holds the `DepositsFacade` and NOTHING the facade already owns: no
25399
+ * `UserLending`, no chain id, no signer. Two paths to one behaviour is how the
25400
+ * KYC params a flow built came to differ from the ones `kasu.deposits`
25401
+ * built — the same class of drift the flows themselves exist to end.
25402
+ */
25403
+ class FlowsFacade {
25404
+ constructor(_deposits,
25405
+ /**
25406
+ * The chain's stable token, bound to whatever the Kasu instance holds.
25407
+ * A factory rather than a contract: it is one `new Contract`, and a
25408
+ * cached binding would outlive the config it was built from.
25409
+ */
25410
+ _erc20,
25411
+ /** The read-only flag `Kasu` already computed — never re-derived here. */
25412
+ _isReadOnly,
25413
+ /**
25414
+ * The ERC-20 spender every deposit run approves: this chain's
25415
+ * `LendingPoolManager`, which is the only contract the default deposit
25416
+ * port calls. Passed to the flow so a consumer never has to hand-wire
25417
+ * an address whose only wrong value grants an approval to the wrong
25418
+ * contract.
25419
+ */
25420
+ _spender) {
25421
+ this._deposits = _deposits;
25422
+ this._erc20 = _erc20;
25423
+ this._isReadOnly = _isReadOnly;
25424
+ this._spender = _spender;
25425
+ }
25426
+ /**
25427
+ * A deposit pipeline. `readAllowance`, `approve`, `deposit` and
25428
+ * `buildKycParams` default to the SDK's own implementations; pass any of
25429
+ * them to override (kasu-ui approves through its sponsored-gas path, for
25430
+ * one).
25431
+ *
25432
+ * Each default is applied per key with `??`, not by spreading `ports` over
25433
+ * them: `{ ...defaults, ...ports }` lets an EXPLICITLY undefined value
25434
+ * delete the default it was meant to keep, and
25435
+ * `approve: sponsoredGas ? sponsoredApprove : undefined` is exactly how a
25436
+ * consumer writes a conditional override.
25437
+ */
25438
+ deposit(ports, opts) {
25439
+ var _a, _b, _c, _d, _e;
25440
+ return new DepositFlow({
25441
+ signMessage: ports.signMessage,
25442
+ generateContract: ports.generateContract,
25443
+ getKycSignature: ports.getKycSignature,
25444
+ buildKycParams: (_a = ports.buildKycParams) !== null && _a !== void 0 ? _a : ((userAddress) => this._deposits.buildKycParams(userAddress)),
25445
+ readAllowance: (_b = ports.readAllowance) !== null && _b !== void 0 ? _b : ((owner, spender) => this._erc20().allowance(owner, spender)),
25446
+ approve: (_c = ports.approve) !== null && _c !== void 0 ? _c : ((spender, amount) => __awaiter(this, void 0, void 0, function* () {
25447
+ this._assertWritable();
25448
+ // The EXACT amount the flow asked for. Nothing here
25449
+ // rounds it up, and nothing here substitutes
25450
+ // MaxUint256.
25451
+ return yield this._erc20().approve(spender, amount);
25452
+ })),
25453
+ deposit: (_d = ports.deposit) !== null && _d !== void 0 ? _d : ((params) => this._deposits.deposit(params)),
25454
+ now: ports.now,
25455
+ }, {
25456
+ contractTtlMs: opts === null || opts === void 0 ? void 0 : opts.contractTtlMs,
25457
+ // Per key here too, for the same reason the ports are: an
25458
+ // explicit `spender: undefined` must not delete the default.
25459
+ spender: (_e = opts === null || opts === void 0 ? void 0 : opts.spender) !== null && _e !== void 0 ? _e : this._spender,
25460
+ });
25461
+ }
25462
+ /**
25463
+ * A withdrawal pipeline. Both write ports and `buildKycParams` default to
25464
+ * this instance; supplying `getKycSignature` turns the KYC pre-check on.
25465
+ * Defaults are applied per key, for the reason `deposit()` gives.
25466
+ */
25467
+ withdraw(ports = {}) {
25468
+ var _a, _b, _c;
25469
+ return new WithdrawFlow({
25470
+ buildKycParams: (_a = ports.buildKycParams) !== null && _a !== void 0 ? _a : ((userAddress) => this._deposits.buildKycParams(userAddress)),
25471
+ getKycSignature: ports.getKycSignature,
25472
+ withdraw: (_b = ports.withdraw) !== null && _b !== void 0 ? _b : ((params) => this._deposits.withdraw({
25473
+ poolId: params.poolId,
25474
+ trancheId: params.trancheId,
25475
+ amount: params.amount,
25476
+ })),
25477
+ withdrawMax: (_c = ports.withdrawMax) !== null && _c !== void 0 ? _c : ((poolId, trancheId, userAddress) => this._deposits.withdrawMax(poolId, trancheId, userAddress)),
25478
+ });
25479
+ }
25480
+ _assertWritable() {
25481
+ if (this._isReadOnly) {
25482
+ throw new Error(READ_ONLY_MESSAGE);
25483
+ }
25484
+ }
25485
+ }
25486
+
24419
25487
  /**
24420
25488
  * Minimum remaining capacity (in stable-asset units) for a tranche to be
24421
25489
  * selectable. Mirrors `MIN_CAPACITY = toBigNumber('1')` in kasu-fe-next's
@@ -24835,6 +25903,230 @@ class StrategiesFacade {
24835
25903
  }
24836
25904
  }
24837
25905
 
25906
+ var UserRequestStatus;
25907
+ (function (UserRequestStatus) {
25908
+ UserRequestStatus["REQUESTED"] = "Requested";
25909
+ UserRequestStatus["PROCESSING"] = "Processing";
25910
+ UserRequestStatus["PROCESSED"] = "Processed";
25911
+ })(UserRequestStatus || (UserRequestStatus = {}));
25912
+
25913
+ /**
25914
+ * Events that represent a lender SUBMISSION into the bundle. A dNFT position
25915
+ * aggregates every submission the lender made into the same pool/tranche this
25916
+ * cycle: the first is `Initiated`, each subsequent top-up is `Increased`.
25917
+ * Everything else on the timeline (Accepted / Rejected / Cancelled /
25918
+ * Reallocated / Forced) is an OUTCOME, not a request, and must not be counted.
25919
+ */
25920
+ const SUBMISSION_EVENTS = new Set(['Initiated', 'Increased']);
25921
+ /**
25922
+ * The submissions bundled into one dNFT-aggregate request row, input order
25923
+ * preserved — one loan agreement per submission.
25924
+ */
25925
+ function submissionEvents(events) {
25926
+ return events.filter((e) => SUBMISSION_EVENTS.has(e.requestType));
25927
+ }
25928
+ /** Count the submissions bundled into one dNFT-aggregate request row. */
25929
+ function countSubmissions(events) {
25930
+ return submissionEvents(events).length;
25931
+ }
25932
+ /**
25933
+ * Timestamp of the FIRST submission in the bundle. Falls back to `fallback`
25934
+ * when the `Initiated` event has not indexed yet — the caller decides what
25935
+ * that is (kasu-ui passes the request's own timestamp).
25936
+ */
25937
+ function firstSubmissionTimestamp(events, fallback) {
25938
+ const submissions = submissionEvents(events);
25939
+ if (submissions.length === 0)
25940
+ return fallback;
25941
+ return submissions.reduce((min, e) => (e.timestamp < min ? e.timestamp : min), Infinity);
25942
+ }
25943
+ /**
25944
+ * The LATEST event timestamp, or `fallback` when the timeline is empty — or
25945
+ * when every event predates it, because `fallback` seeds the reduction. That
25946
+ * seeding is deliberate and is kasu-ui's behaviour: `request.timestamp` is a
25947
+ * fact about the request, and an event indexed with an earlier clock must not
25948
+ * make the row look older than the request itself.
25949
+ */
25950
+ function lastEventTimestamp(events, fallback) {
25951
+ return events.reduce((max, e) => (e.timestamp > max ? e.timestamp : max), fallback);
25952
+ }
25953
+ /**
25954
+ * Has THIS request's cycle closed? — the single open/closed signal behind the
25955
+ * status vocabulary and behind Cancel.
25956
+ *
25957
+ * `request.canCancel` is the SDK's `isCancelable(status, poolId)` —
25958
+ * `status !== 'Processed' && !isLendingPoolClearingPending(pool)`. It is
25959
+ * per-POOL and reads the same condition the contract enforces on the cancel
25960
+ * call — but it reads it ONCE, when the request was fetched. Nothing about
25961
+ * this value is live, so a client that holds a request across a cycle close
25962
+ * must refetch before acting on it.
25963
+ *
25964
+ * The raw subgraph `status` must NOT feed this: `'Processing'` is a STICKY
25965
+ * historical marker set on the first partial fill and never reset, so gating
25966
+ * on it would freeze a partly-filled request in Processing forever. A global
25967
+ * settlement clock is equally wrong here — it is blind to whether THIS
25968
+ * request's pool is already clearing.
25969
+ */
25970
+ function isCycleClosed(request) {
25971
+ return !request.canCancel;
25972
+ }
25973
+ const isCancelled = (events) => events.some((e) => e.requestType === 'Cancelled');
25974
+ const isForced = (events) => events.some((e) => e.requestType === 'Forced');
25975
+ const initiatedEvent = (events) => events.find((e) => e.requestType === 'Initiated');
25976
+ /**
25977
+ * Subgraph behaviour: when a request is cancelled, the on-request
25978
+ * `requestedAmount` field is reset to 0 (the lender's effective balance is
25979
+ * restored). The original amount survives on the `Initiated` event's
25980
+ * `assetAmount`. Recover from there so cancelled rows still carry the amount
25981
+ * the lender originally asked for.
25982
+ */
25983
+ const initiatedAmountOf = (events) => {
25984
+ const initiated = initiatedEvent(events);
25985
+ return initiated ? Number(initiated.assetAmount || '0') : 0;
25986
+ };
25987
+ /**
25988
+ * A deposit is REALLOCATED when the timeline carries a `Reallocated` event, or
25989
+ * an `Accepted` event into a tranche other than the one requested.
25990
+ */
25991
+ const findReallocation = (events, originalTrancheId) => events.find((e) => e.requestType === 'Reallocated' ||
25992
+ (e.requestType === 'Accepted' &&
25993
+ e.trancheId.toLowerCase() !==
25994
+ originalTrancheId.toLowerCase()));
25995
+ /** kasu-ui's `num`: an absent or unparseable figure reads as 0. */
25996
+ const num = (str) => {
25997
+ const n = Number(str !== null && str !== void 0 ? str : '0');
25998
+ return Number.isFinite(n) ? n : 0;
25999
+ };
26000
+ /** The same parse, keeping "the subgraph reported nothing" distinct from 0. */
26001
+ const numOrNull = (str) => {
26002
+ if (str === undefined || str === null || str.trim() === '')
26003
+ return null;
26004
+ const n = Number(str);
26005
+ return Number.isFinite(n) ? n : null;
26006
+ };
26007
+ /**
26008
+ * Convert a `UserRequest` into a `RequestState`. Pure — no clock, no network,
26009
+ * no copy.
26010
+ *
26011
+ * BRANCH ORDER (this IS the behaviour; it reproduces kasu-ui's
26012
+ * `deriveTransactionView` check for check):
26013
+ *
26014
+ * 1. a `Cancelled` event → `cancelled`, neutral, 0
26015
+ * 2. a withdrawal with a `Forced` event → `forced`, outflow, −accepted
26016
+ * 3. a reallocated deposit → `reallocated`, inflow, +accepted
26017
+ * 4. a withdrawal partly filled with a LIVE
26018
+ * remainder (cycle still open) → `partial`, outflow, −accepted
26019
+ * 5. resolved (`status === 'Processed'`):
26020
+ * withdrawal, partly filled → `partial`, outflow, −accepted
26021
+ * withdrawal, fully filled → `complete`, outflow, −accepted
26022
+ * deposit, nothing accepted → `rejected`, neutral, 0
26023
+ * deposit, part rejected → `partial`, inflow, +accepted
26024
+ * deposit, fully accepted → `complete`, inflow, +accepted
26025
+ * 6. otherwise → `pending`, ±requested
26026
+ *
26027
+ * `cancelled` is reachable ONLY from branch 1 — a Cancelled EVENT. No
26028
+ * processing or resolved state can derive it.
26029
+ *
26030
+ * Branch 4 is checked BEFORE the resolved branch and gates on `isCycleClosed`
26031
+ * (i.e. `canCancel`), never on the sticky raw status: a withdrawal that was
26032
+ * partly filled returns to the queue with a live Cancel, and reading the raw
26033
+ * status would strand it.
26034
+ *
26035
+ * WHAT THE APPLICATION STILL OWNS: the status word and the detail line beneath
26036
+ * it; the tranche display rename (`getTrancheDisplayName` on `trancheName`
26037
+ * and on `reallocationTargetTrancheName`, both of which are RAW here); the
26038
+ * pool-name split; the amount format. The "view loan agreement" affordance is
26039
+ * a fact, and it follows from two fields already here —
26040
+ * `requestType === 'Deposit' && statusCode !== 'cancelled' && statusCode !== 'rejected'`
26041
+ * — because neither a cancelled nor a fully-rejected deposit ever issued one,
26042
+ * and withdrawals sign no agreement at all.
26043
+ */
26044
+ function deriveRequestState(request) {
26045
+ var _a, _b, _c;
26046
+ const isWithdrawal = request.requestType === 'Withdrawal';
26047
+ const cancelled = isCancelled(request.events);
26048
+ // Cancelled requests have `requestedAmount` zeroed on the request itself;
26049
+ // pull the original value from the Initiated event so the row still
26050
+ // carries "100 cancelled" instead of "0 cancelled".
26051
+ const requested = cancelled
26052
+ ? num(request.requestedAmount) || initiatedAmountOf(request.events)
26053
+ : num(request.requestedAmount);
26054
+ const accepted = num(request.acceptedAmount);
26055
+ const rejected = num(request.rejectedAmount);
26056
+ const forced = isWithdrawal && isForced(request.events);
26057
+ const reallocation = !isWithdrawal && findReallocation(request.events, request.trancheId);
26058
+ const initiated = initiatedEvent(request.events);
26059
+ const base = {
26060
+ id: request.id,
26061
+ contractId: (_a = initiated === null || initiated === void 0 ? void 0 : initiated.id) !== null && _a !== void 0 ? _a : '',
26062
+ poolId: request.lendingPool.id.toLowerCase(),
26063
+ poolName: request.lendingPool.name,
26064
+ trancheName: request.trancheName,
26065
+ trancheId: request.trancheId,
26066
+ fixedTermConfigId: (_c = (_b = request.fixedTermConfig) === null || _b === void 0 ? void 0 : _b.configId) !== null && _c !== void 0 ? _c : '0',
26067
+ requestType: request.requestType,
26068
+ rawStatus: request.status,
26069
+ requestedAmount: requested,
26070
+ acceptedAmount: numOrNull(request.acceptedAmount),
26071
+ initiatedAmount: initiated
26072
+ ? initiatedAmountOf(request.events)
26073
+ : null,
26074
+ rejectedAmount: rejected,
26075
+ reallocatedOutAmount: reallocation ? num(reallocation.assetAmount) : 0,
26076
+ reallocationTargetTrancheName: reallocation
26077
+ ? reallocation.trancheName
26078
+ : null,
26079
+ lastTimestamp: lastEventTimestamp(request.events, request.timestamp),
26080
+ submissionCount: countSubmissions(request.events),
26081
+ // `firstSubmissionTimestamp` needs a fallback it will never use here:
26082
+ // the bundle is non-empty on every path that reaches the call.
26083
+ firstSubmissionTimestamp: countSubmissions(request.events) === 0
26084
+ ? null
26085
+ : firstSubmissionTimestamp(request.events, 0),
26086
+ cycleClosed: isCycleClosed(request),
26087
+ canCancel: request.canCancel,
26088
+ };
26089
+ // 1. Cancelled wins over everything — the ONLY path to `cancelled`.
26090
+ if (cancelled) {
26091
+ return Object.assign(Object.assign({}, base), { kind: 'neutral', amount: 0, statusCode: 'cancelled' });
26092
+ }
26093
+ // 2. Forced withdrawal (credit originator returned funds early).
26094
+ if (forced) {
26095
+ return Object.assign(Object.assign({}, base), { kind: 'outflow', amount: -accepted, statusCode: 'forced' });
26096
+ }
26097
+ // 3. Reallocated deposit (accepted into a different lending option).
26098
+ if (reallocation) {
26099
+ return Object.assign(Object.assign({}, base), { kind: 'inflow', amount: accepted, statusCode: 'reallocated' });
26100
+ }
26101
+ // 4. Withdrawal partly filled with a LIVE remainder: the request returns
26102
+ // to the queue with a live Cancel. Checked BEFORE the resolved branch and
26103
+ // via `isCycleClosed`, never via the sticky raw status.
26104
+ if (isWithdrawal &&
26105
+ accepted > 0 &&
26106
+ accepted < requested &&
26107
+ !isCycleClosed(request)) {
26108
+ return Object.assign(Object.assign({}, base), { kind: 'outflow', amount: -accepted, statusCode: 'partial' });
26109
+ }
26110
+ // 5. Resolved — a rejection IS a resolution.
26111
+ if (request.status === UserRequestStatus.PROCESSED) {
26112
+ if (isWithdrawal) {
26113
+ const partly = accepted < requested;
26114
+ return Object.assign(Object.assign({}, base), { kind: 'outflow', amount: -accepted, statusCode: partly ? 'partial' : 'complete' });
26115
+ }
26116
+ // Deposit: full reject vs partial vs full accept.
26117
+ if (accepted === 0) {
26118
+ return Object.assign(Object.assign({}, base), { kind: 'neutral', amount: 0, statusCode: 'rejected' });
26119
+ }
26120
+ if (rejected > 0) {
26121
+ return Object.assign(Object.assign({}, base), { kind: 'inflow', amount: accepted, statusCode: 'partial' });
26122
+ }
26123
+ return Object.assign(Object.assign({}, base), { kind: 'inflow', amount: accepted, statusCode: 'complete' });
26124
+ }
26125
+ // 6. Unresolved. `cycleClosed` tells the caller whether to render its
26126
+ // "queued" or its "processing" word; the code is the same either way.
26127
+ return Object.assign(Object.assign({}, base), { kind: isWithdrawal ? 'outflow' : 'inflow', amount: isWithdrawal ? -requested : requested, statusCode: 'pending' });
26128
+ }
26129
+
24838
26130
  /**
24839
26131
  * High-level facade for querying a user's portfolio, positions, and history.
24840
26132
  *
@@ -24877,13 +26169,36 @@ class PortfolioFacade {
24877
26169
  return yield this._userLending.getUserRequests(userAddress, currentEpoch);
24878
26170
  });
24879
26171
  }
26172
+ /**
26173
+ * The same history, already derived into `RequestState` rows — status
26174
+ * code, kind, signed amount, the cancelled-amount recovery, the bundled
26175
+ * submission count and the cycle-open signal.
26176
+ *
26177
+ * ```ts
26178
+ * const rows = await kasu.portfolio.getRequestStates('0xUser...');
26179
+ * rows.filter((r) => r.statusCode === 'pending');
26180
+ * ```
26181
+ *
26182
+ * Every application derives this from `getTransactionHistory` anyway, and
26183
+ * the derivation is the part they were each getting subtly differently.
26184
+ * Words are still the caller's: render `statusCode` in your own
26185
+ * vocabulary, and call `getTrancheDisplayName` on `trancheName` at the
26186
+ * view boundary.
26187
+ */
26188
+ getRequestStates(userAddress) {
26189
+ return __awaiter(this, void 0, void 0, function* () {
26190
+ const requests = yield this.getTransactionHistory(userAddress);
26191
+ return requests.map(deriveRequestState);
26192
+ });
26193
+ }
24880
26194
  }
24881
26195
 
24882
26196
  /**
24883
26197
  * High-level entry point for external integrators.
24884
26198
  *
24885
- * Provides three domain facades — `strategies`, `deposits`, `portfolio` — and
24886
- * exposes the underlying `KasuSdk` services via `.services` for power-users.
26199
+ * Provides four domain facades — `strategies`, `deposits`, `portfolio`,
26200
+ * `flows` — and exposes the underlying `KasuSdk` services via `.services` for
26201
+ * power-users.
24887
26202
  *
24888
26203
  * ```ts
24889
26204
  * import { Kasu } from '@kasufinance/kasu-sdk';
@@ -24907,8 +26222,13 @@ class Kasu {
24907
26222
  this._signerOrProvider = signerOrProvider;
24908
26223
  this._configOverrides = configOverrides;
24909
26224
  this.strategies = new StrategiesFacade(sdk.DataService, sdk.UserLending);
24910
- this.deposits = new DepositsFacade(sdk.UserLending, chainConfig.chainId.toString(), !ethers.Signer.isSigner(signerOrProvider));
26225
+ // Derived ONCE and handed to every facade that needs it. Each of them
26226
+ // re-deriving `Signer.isSigner` is how two facades come to disagree
26227
+ // about whether the same instance can write.
26228
+ const isReadOnly = !ethers.Signer.isSigner(signerOrProvider);
26229
+ this.deposits = new DepositsFacade(sdk.UserLending, chainConfig.chainId.toString(), isReadOnly);
24911
26230
  this.portfolio = new PortfolioFacade(sdk.DataService, sdk.UserLending, sdk.Portfolio);
26231
+ this.flows = new FlowsFacade(this.deposits, () => erc20Of(chainConfig, signerOrProvider), isReadOnly, chainConfig.contracts.LendingPoolManager);
24912
26232
  }
24913
26233
  /**
24914
26234
  * Create a Kasu instance with built-in chain config.
@@ -25040,6 +26360,23 @@ function rpcUrlsOf(chainConfig) {
25040
26360
  var _a;
25041
26361
  return (_a = chainConfig.rpcUrls) !== null && _a !== void 0 ? _a : [];
25042
26362
  }
26363
+ /**
26364
+ * The chain's stable token, bound to whatever this instance holds.
26365
+ *
26366
+ * Built per call rather than cached: it is one `new Contract`, and a cached
26367
+ * binding would outlive the signer a `connect()` replaced. A config with no
26368
+ * `stableAsset` (a hand-written entry from before 2.5.0) throws here rather
26369
+ * than inside ethers, naming the two ports that make the flow work without
26370
+ * one.
26371
+ */
26372
+ function erc20Of(chainConfig, signerOrProvider) {
26373
+ var _a;
26374
+ const address = (_a = stableAssetOf(chainConfig)) === null || _a === void 0 ? void 0 : _a.address;
26375
+ if (!address) {
26376
+ throw new Error('Kasu: this chain config has no stableAsset; pass readAllowance and approve ports explicitly');
26377
+ }
26378
+ return IERC20MetadataAbi__factory.connect(address, signerOrProvider);
26379
+ }
25043
26380
  /**
25044
26381
  * The read-only provider used when the caller passes no `signerOrProvider`.
25045
26382
  *
@@ -25125,6 +26462,160 @@ function fetchUnusedPoolIds() {
25125
26462
  });
25126
26463
  }
25127
26464
 
26465
+ /**
26466
+ * The AU cumulative-lending minimum — the numeric half.
26467
+ *
26468
+ * RULE: a lender whose verified KYC country is Australia may only lend when
26469
+ * their existing deposited position on THIS deployment, plus the amount they
26470
+ * are asking for, reaches the deployment's threshold.
26471
+ *
26472
+ * This is UX PRE-VALIDATION ONLY. kasu-backend enforces the rule
26473
+ * authoritatively at contract generation and refuses with HTTP 403
26474
+ * `AU_WHOLESALE_MINIMUM_NOT_MET`; a lender who gets past this check is still
26475
+ * stopped there. What these helpers exist for is to raise the amount field's
26476
+ * minimum so the lender learns the rule while typing rather than at the end.
26477
+ *
26478
+ * FAIL-OPEN by design: an absent or unknown country is NOT restricted, and an
26479
+ * unlisted stable symbol has no threshold. An UNKNOWN existing position is
26480
+ * treated as 0 — strict, so the floor is never advertised lower than the
26481
+ * backend will accept, and it relaxes once the position loads.
26482
+ *
26483
+ * Arithmetic is integer bigint in minor units (10^decimals) so no two
26484
+ * consumers can disagree at the boundary. Parsing TRUNCATES, never rounds up —
26485
+ * a position can never be inflated into passing. `BigInt(...)` calls only, no
26486
+ * bigint literals, so consumers on an older target still compile.
26487
+ *
26488
+ * Lifted from kasu-ui's `features/lending/lib/au-lending-restriction.ts`. The
26489
+ * two toast strings that live there are copy and stay in the applications.
26490
+ */
26491
+ const ZERO = BigInt(0);
26492
+ /** ISO 3166-1 alpha-3 for Australia — the collapsed KYC country field. */
26493
+ const AU_ALPHA3 = 'AUS';
26494
+ /**
26495
+ * Is this the Australian KYC country? Case-insensitive and
26496
+ * whitespace-tolerant. Anything that is not a string — including the
26497
+ * `undefined` of an unloaded KYC record — is not restricted (fail-open).
26498
+ *
26499
+ * The alpha-2 `'AU'` deliberately does NOT match: the KYC country reaching
26500
+ * this rule is normalised to alpha-3 upstream, and a bare two-letter code
26501
+ * here means something else went wrong.
26502
+ */
26503
+ function isAustralianKyc(country) {
26504
+ if (typeof country !== 'string')
26505
+ return false;
26506
+ return country.trim().toUpperCase() === AU_ALPHA3;
26507
+ }
26508
+ /**
26509
+ * Minimum CUMULATIVE position, in WHOLE stable units, keyed on the
26510
+ * deployment's stable-asset symbol (a property of the currency, not the
26511
+ * chain). Must match kasu-backend's table exactly.
26512
+ */
26513
+ const AU_MIN_CUMULATIVE_BY_STABLE = {
26514
+ USDC: 360000,
26515
+ AUDD: 500000,
26516
+ };
26517
+ /**
26518
+ * Threshold in whole units, or `undefined` for an unlisted stable — a
26519
+ * deployment whose currency has no configured minimum is unrestricted.
26520
+ */
26521
+ function auThresholdFor(symbol) {
26522
+ if (typeof symbol !== 'string')
26523
+ return undefined;
26524
+ return AU_MIN_CUMULATIVE_BY_STABLE[symbol.trim().toUpperCase()];
26525
+ }
26526
+ /**
26527
+ * Is this wallet released from the minimum entirely, whatever its country,
26528
+ * stable or position says?
26529
+ *
26530
+ * `exemptAddresses` is DEPLOYMENT CONFIGURATION the caller supplies, not a
26531
+ * constant of this package. kasu-sdk is published publicly; the exempt list is
26532
+ * a compliance carve-out naming particular lender wallets, so it does not
26533
+ * belong in a public tarball. Read it from wherever the application keeps its
26534
+ * deployment configuration, and keep it identical to kasu-backend's
26535
+ * `AU_WHOLESALE_EXEMPT_ADDRESSES` — that is where the rule is actually
26536
+ * enforced. An address exempt here but not there sees no raised minimum in the
26537
+ * form and is refused at the end, which is worse than not exempting it at all.
26538
+ *
26539
+ * The address matched must be the CONNECTED wallet — the one that owns the KYC
26540
+ * record and signs the agreement request. kasu-backend verifies that signature
26541
+ * before the gate, so an exemption cannot be claimed by asserting someone
26542
+ * else's address; it takes their private key. Never match a "view as" address.
26543
+ *
26544
+ * Comparison is case-insensitive and whitespace-tolerant. Anything that is not
26545
+ * a string is NOT exempt (fail closed), and so is anything when the list is
26546
+ * absent.
26547
+ */
26548
+ function isAuMinimumExempt(address, exemptAddresses) {
26549
+ if (typeof address !== 'string' || !exemptAddresses)
26550
+ return false;
26551
+ const needle = address.trim().toLowerCase();
26552
+ return exemptAddresses.some((a) => a.trim().toLowerCase() === needle);
26553
+ }
26554
+ const DECIMAL_RE = /^(\d+)?(?:\.(\d*))?$/;
26555
+ /**
26556
+ * Truncating, partial-input-tolerant decimal → minor-unit parse.
26557
+ * `'10.'` → `10000000n` · `'1.23456789'` → `1234567n` (truncated at 6dp).
26558
+ * `''` / `'abc'` / negative / null → `null`.
26559
+ *
26560
+ * Tolerant of mid-typing states because it runs on an amount field as the
26561
+ * lender types, and truncating because rounding up would let a position that
26562
+ * is a fraction short read as passing.
26563
+ */
26564
+ function parseMinorUnits(value, decimals) {
26565
+ if (value === null || value === undefined)
26566
+ return null;
26567
+ const raw = typeof value === 'number' ? String(value) : value.trim();
26568
+ if (!raw)
26569
+ return null;
26570
+ const match = DECIMAL_RE.exec(raw);
26571
+ if (!match || (!match[1] && !match[2]))
26572
+ return null;
26573
+ // Both capture groups are optional, so either can be absent at runtime.
26574
+ // A fractional group that matched EMPTY ('10.') is not absent, and the
26575
+ // destructuring defaults leave it alone — only `undefined` takes them.
26576
+ const [, whole = '0', fracDigits = ''] = match;
26577
+ const frac = fracDigits.slice(0, decimals).padEnd(decimals, '0');
26578
+ return BigInt(whole + frac);
26579
+ }
26580
+ /**
26581
+ * The amount, in WHOLE stable units, an Australian lender still needs in order
26582
+ * to reach the deployment's cumulative minimum. Apply it as the amount field's
26583
+ * floor via `max(trancheMin, auMinimumRemaining(...))`.
26584
+ *
26585
+ * Returns 0 for everyone the rule does not restrict (fail-open), 0 for an
26586
+ * exempt address, and 0 once the lender's existing position already satisfies
26587
+ * the threshold.
26588
+ *
26589
+ * The exemption is tested FIRST and unconditionally: an exempt wallet has no
26590
+ * minimum whatever its country, stable or position says. kasu-backend
26591
+ * deliberately tests country first and the exemption second — the two orders
26592
+ * are not in conflict, both return "no minimum" for the same inputs. There,
26593
+ * the exemption sets a flag that drives a compliance log, so it must not fire
26594
+ * for a lender the rule never engaged for. Here nothing is logged, so
26595
+ * exemption-first is preferred: it makes this function right on its own,
26596
+ * whatever country a caller happens to pass.
26597
+ */
26598
+ function auMinimumRemaining(input) {
26599
+ var _a, _b;
26600
+ if (isAuMinimumExempt(input.address, input.exemptAddresses))
26601
+ return 0;
26602
+ if (!isAustralianKyc(input.country))
26603
+ return 0;
26604
+ const threshold = auThresholdFor(input.stableSymbol);
26605
+ if (threshold === undefined)
26606
+ return 0;
26607
+ const { existingDeposited, decimals } = input;
26608
+ // A table threshold always parses; `?? ZERO` keeps the rule fail-open
26609
+ // rather than asserting, so an unparseable one yields no minimum at all.
26610
+ const thresholdMinor = (_a = parseMinorUnits(threshold, decimals)) !== null && _a !== void 0 ? _a : ZERO;
26611
+ const existingMinor = (_b = parseMinorUnits(existingDeposited, decimals)) !== null && _b !== void 0 ? _b : ZERO;
26612
+ const remaining = thresholdMinor - existingMinor;
26613
+ if (remaining <= ZERO)
26614
+ return 0;
26615
+ const base = BigInt('1' + '0'.repeat(decimals));
26616
+ return Number(remaining / base) + Number(remaining % base) / Number(base);
26617
+ }
26618
+
25128
26619
  // Fallbacks if the selected tranche's on-chain min/max are 0 / NaN (some
25129
26620
  // "coming soon" pools haven't been configured yet).
25130
26621
  const MIN_LENDING_AMOUNT_FALLBACK = 500;
@@ -25252,6 +26743,123 @@ function getInstitutionalLender(originator) {
25252
26743
  return originator.name === APXIUM.name ? RIXON_CAPITAL : null;
25253
26744
  }
25254
26745
 
26746
+ /**
26747
+ * Cycles and the clearing window — the protocol's weekly clock, as numbers.
26748
+ *
26749
+ * Lifted verbatim from kasu-ui's `features/portfolio/lib/settlement-window.ts`
26750
+ * and `features/lending/lib/cycle-dates.ts`. The two formatters that live
26751
+ * beside `deriveCycleDates` there (`formatCycleDate`, `formatCycleCloseUtc`)
26752
+ * print words and stay in the applications; everything here is unix seconds in
26753
+ * and unix seconds out.
26754
+ *
26755
+ * The clearing window is the fixed 48 hours immediately preceding an epoch
26756
+ * end. Inside it, pending requests are being processed and cannot be modified,
26757
+ * and the countdown runs to the epoch end. Outside it, the countdown runs to
26758
+ * the next clearing-window start.
26759
+ *
26760
+ * The epoch end comes straight from the chain (`nextEpochStartTimestamp`, i.e.
26761
+ * the SDK's `getNextEpochDate`) — the same value the protocol's own
26762
+ * `getNextClearingPeriodDate` derives from — so the window always lines up
26763
+ * with the real weekly schedule (Tue 06:00 → Thu 06:00 UTC on Base) instead of
26764
+ * a projected subgraph timestamp that can drift off the grid.
26765
+ */
26766
+ /**
26767
+ * Clearing-window length — a fixed 48h protocol constant. Exported so every
26768
+ * consumer derives the window from the same number this module runs on.
26769
+ */
26770
+ const CLEARING_WINDOW_SECONDS = 48 * 60 * 60;
26771
+ /**
26772
+ * Weekly cadence — the epoch schedule is fixed weekly (Tue → Thu UTC on Base).
26773
+ * Used only to roll a cycle forward when a request lands inside a window that
26774
+ * has already closed.
26775
+ */
26776
+ const WEEK_SECONDS = 7 * 24 * 60 * 60;
26777
+ /**
26778
+ * Which phase of the weekly cycle `nowSeconds` falls in, and how long is left
26779
+ * of it.
26780
+ *
26781
+ * `'unknown'` when no epoch boundary has been loaded yet, or when the one on
26782
+ * hand is stale (it has already elapsed — the on-chain value refetches to the
26783
+ * next boundary shortly after rollover). A caller must render its "no cycle
26784
+ * loaded" state there, never a zeroed countdown.
26785
+ */
26786
+ function computeSettlementWindow({ nowSeconds, nextEpochStart, clearingWindowSeconds = CLEARING_WINDOW_SECONDS, }) {
26787
+ // No epoch boundary loaded yet, or a stale one that already elapsed.
26788
+ if (nextEpochStart <= 0 || nextEpochStart <= nowSeconds) {
26789
+ return { phase: 'unknown' };
26790
+ }
26791
+ const clearingStart = nextEpochStart - clearingWindowSeconds;
26792
+ if (nowSeconds < clearingStart) {
26793
+ return {
26794
+ phase: 'awaiting',
26795
+ secondsUntilClearing: clearingStart - nowSeconds,
26796
+ nextClearingStart: clearingStart,
26797
+ };
26798
+ }
26799
+ // nowSeconds is in [clearingStart, nextEpochStart) — inside the window.
26800
+ return {
26801
+ phase: 'clearing',
26802
+ secondsUntilEpochEnd: nextEpochStart - nowSeconds,
26803
+ epochEnd: nextEpochStart,
26804
+ };
26805
+ }
26806
+ /**
26807
+ * The next cycle boundary strictly after `nowSeconds`, in unix seconds — the
26808
+ * cycle close (`nextEpochStart − 48h`) while the window is still open, the
26809
+ * epoch end once we are inside it.
26810
+ *
26811
+ * `undefined` when there is no boundary left to wait for: no epoch boundary
26812
+ * loaded, or a cached one that has already elapsed — the same staleness rule
26813
+ * `computeSettlementWindow` applies before it reports `'unknown'`.
26814
+ *
26815
+ * Split out of the state machine because some consumers need the INSTANT
26816
+ * rather than the phase: one to flush the cycle-dependent caches when the
26817
+ * clock crosses it, one to move a pre-commit screen's snapshot clock at the
26818
+ * same moment.
26819
+ */
26820
+ function nextCycleBoundary(nextEpochStart, nowSeconds, clearingWindowSeconds = CLEARING_WINDOW_SECONDS) {
26821
+ if (!nextEpochStart || nextEpochStart <= 0)
26822
+ return undefined;
26823
+ const clearingStart = nextEpochStart - clearingWindowSeconds;
26824
+ if (nowSeconds < clearingStart)
26825
+ return clearingStart;
26826
+ if (nowSeconds < nextEpochStart)
26827
+ return nextEpochStart;
26828
+ return undefined;
26829
+ }
26830
+ /**
26831
+ * The cycle-close and outcome dates for a request submitted `now`.
26832
+ *
26833
+ * - The cycle "closes" (stops accepting requests, starts processing) at the
26834
+ * start of the 48h clearing window, i.e. 48h before the epoch end.
26835
+ * - Processing takes up to 48h, so the outcome is confirmed by the epoch end
26836
+ * (close + 48h).
26837
+ *
26838
+ * Returns `null` when the epoch boundary is not available or is stale — a
26839
+ * caller then omits the dates entirely (omit, don't stub).
26840
+ *
26841
+ * The common case is a request submitted OUTSIDE the clearing window: the
26842
+ * close is `nextEpochStart − 48h` and the outcome is `nextEpochStart`. When
26843
+ * the request lands INSIDE the current clearing window (that close is already
26844
+ * in the past), it queues for the NEXT weekly cycle, so the close is advanced
26845
+ * by whole weeks until it is in the future.
26846
+ */
26847
+ function deriveCycleDates(nextEpochStart, nowSeconds) {
26848
+ // No boundary loaded, or a stale one that already elapsed — the same
26849
+ // staleness rule as `computeSettlementWindow`.
26850
+ if (!nextEpochStart || nextEpochStart <= nowSeconds)
26851
+ return null;
26852
+ let close = nextEpochStart - CLEARING_WINDOW_SECONDS;
26853
+ let outcome = nextEpochStart;
26854
+ // Inside the current clearing window the close already passed; a request
26855
+ // now is queued for the next weekly cycle.
26856
+ while (close <= nowSeconds) {
26857
+ close += WEEK_SECONDS;
26858
+ outcome += WEEK_SECONDS;
26859
+ }
26860
+ return { close, outcome };
26861
+ }
26862
+
25255
26863
  // Business rename (2026-07): Apxium strategies market their top retail tranche
25256
26864
  // as "Upper Mezzanine" — the true senior position is held by the institutional
25257
26865
  // lender (Rixon Capital), so retail lenders are never actually senior in the
@@ -25282,34 +26890,71 @@ function getTrancheDisplayName(trancheName, pool) {
25282
26890
  }
25283
26891
 
25284
26892
  exports.APXIUM = APXIUM;
26893
+ exports.AU_ALPHA3 = AU_ALPHA3;
26894
+ exports.AU_MIN_CUMULATIVE_BY_STABLE = AU_MIN_CUMULATIVE_BY_STABLE;
25285
26895
  exports.CHAIN_CONFIGS = CHAIN_CONFIGS;
26896
+ exports.CLEARING_WINDOW_SECONDS = CLEARING_WINDOW_SECONDS;
26897
+ exports.CONTRACT_TTL_MS = CONTRACT_TTL_MS;
26898
+ exports.DepositFlow = DepositFlow;
25286
26899
  exports.DepositsFacade = DepositsFacade;
25287
26900
  exports.EPOCHS_IN_YEAR = EPOCHS_IN_YEAR;
26901
+ exports.Flow = Flow;
26902
+ exports.FlowStore = FlowStore;
26903
+ exports.FlowsFacade = FlowsFacade;
25288
26904
  exports.INVOICEMATE = INVOICEMATE;
25289
26905
  exports.Kasu = Kasu;
25290
26906
  exports.KasuSdk = KasuSdk;
25291
26907
  exports.MAX_LENDING_AMOUNT_FALLBACK = MAX_LENDING_AMOUNT_FALLBACK;
25292
26908
  exports.MIN_LENDING_AMOUNT_FALLBACK = MIN_LENDING_AMOUNT_FALLBACK;
25293
26909
  exports.MIN_TRANCHE_CAPACITY = MIN_TRANCHE_CAPACITY;
26910
+ exports.NO_DIRECTUS_URL_MESSAGE = NO_DIRECTUS_URL_MESSAGE;
26911
+ exports.NO_KYC_PARAMS_MESSAGE = NO_KYC_PARAMS_MESSAGE;
26912
+ exports.NO_SPENDER_MESSAGE = NO_SPENDER_MESSAGE;
25294
26913
  exports.PortfolioFacade = PortfolioFacade;
26914
+ exports.READ_ONLY_MESSAGE = READ_ONLY_MESSAGE;
25295
26915
  exports.RIXON_CAPITAL = RIXON_CAPITAL;
25296
26916
  exports.SdkConfig = SdkConfig;
25297
26917
  exports.StrategiesFacade = StrategiesFacade;
25298
26918
  exports.UPPER_MEZZANINE = UPPER_MEZZANINE;
26919
+ exports.WithdrawFlow = WithdrawFlow;
25299
26920
  exports.apyToEpochRate = apyToEpochRate;
26921
+ exports.asContractType = asContractType;
26922
+ exports.auMinimumRemaining = auMinimumRemaining;
26923
+ exports.auThresholdFor = auThresholdFor;
26924
+ exports.buildContractVersionType = buildContractVersionType;
26925
+ exports.buildFullNameRequestMessage = buildFullNameRequestMessage;
26926
+ exports.buildLegacyContractRequestMessage = buildLegacyContractRequestMessage;
26927
+ exports.buildLoanAgreementSignMessage = buildLoanAgreementSignMessage;
25300
26928
  exports.ceilToCents = ceilToCents;
26929
+ exports.classifyWalletFailure = classifyWalletFailure;
25301
26930
  exports.compareTrancheSeniority = compareTrancheSeniority;
26931
+ exports.computeSettlementWindow = computeSettlementWindow;
26932
+ exports.countSubmissions = countSubmissions;
26933
+ exports.deriveCycleDates = deriveCycleDates;
25302
26934
  exports.derivePoolStatus = derivePoolStatus;
26935
+ exports.deriveRequestState = deriveRequestState;
26936
+ exports.encodeDepositData = encodeDepositData;
25303
26937
  exports.epochRateToApy = epochRateToApy;
25304
26938
  exports.fetchUnusedPoolIds = fetchUnusedPoolIds;
26939
+ exports.firstSubmissionTimestamp = firstSubmissionTimestamp;
25305
26940
  exports.floorToCents = floorToCents;
26941
+ exports.formatSignTimestampUtc = formatSignTimestampUtc;
25306
26942
  exports.getCreditOriginator = getCreditOriginator;
25307
26943
  exports.getInstitutionalLender = getInstitutionalLender;
25308
26944
  exports.getTrancheDisplayName = getTrancheDisplayName;
26945
+ exports.isAuMinimumExempt = isAuMinimumExempt;
26946
+ exports.isAustralianKyc = isAustralianKyc;
25309
26947
  exports.isBelowMinimumCapacity = isBelowMinimumCapacity;
26948
+ exports.isCycleClosed = isCycleClosed;
26949
+ exports.isUnpredictableGas = isUnpredictableGas;
26950
+ exports.isUserRejected = isUserRejected;
26951
+ exports.lastEventTimestamp = lastEventTimestamp;
25310
26952
  exports.maxNetRateCeiling = maxNetRateCeiling;
25311
26953
  exports.netEffectiveApy = netEffectiveApy;
25312
26954
  exports.netTrancheApyBounds = netTrancheApyBounds;
26955
+ exports.nextCycleBoundary = nextCycleBoundary;
26956
+ exports.parseFormattedMessage = parseFormattedMessage;
26957
+ exports.parseMinorUnits = parseMinorUnits;
25313
26958
  exports.parseTrancheBound = parseTrancheBound;
25314
26959
  exports.pickDefaultTrancheId = pickDefaultTrancheId;
25315
26960
  exports.pickHighestYieldTranche = pickHighestYieldTranche;
@@ -25318,6 +26963,7 @@ exports.poolMaxApy = poolMaxApy;
25318
26963
  exports.resolveBoundShortcuts = resolveBoundShortcuts;
25319
26964
  exports.resolveDepositBounds = resolveDepositBounds;
25320
26965
  exports.selectVisiblePools = selectVisiblePools;
26966
+ exports.submissionEvents = submissionEvents;
25321
26967
  exports.trancheApyBounds = trancheApyBounds;
25322
26968
  exports.trancheHasCapacity = trancheHasCapacity;
25323
26969
  exports.trancheRiskRank = trancheRiskRank;