@kasufinance/kasu-sdk 2.8.0 → 2.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/README.md +120 -9
  2. package/dist/bundle.cjs.js +2077 -476
  3. package/dist/bundle.esm.js +2039 -478
  4. package/dist/domain/batch-proof.d.ts +210 -0
  5. package/dist/domain/batch-proof.js +384 -0
  6. package/dist/domain/batch-proof.js.map +1 -0
  7. package/dist/domain/explorer-urls.d.ts +33 -0
  8. package/dist/domain/explorer-urls.js +38 -0
  9. package/dist/domain/explorer-urls.js.map +1 -0
  10. package/dist/domain/fixed-term.d.ts +74 -0
  11. package/dist/domain/fixed-term.js +86 -0
  12. package/dist/domain/fixed-term.js.map +1 -0
  13. package/dist/domain/index.d.ts +12 -2
  14. package/dist/domain/index.js +7 -2
  15. package/dist/domain/index.js.map +1 -1
  16. package/dist/domain/partners.d.ts +29 -0
  17. package/dist/domain/partners.js +20 -0
  18. package/dist/domain/partners.js.map +1 -1
  19. package/dist/domain/requests.d.ts +24 -0
  20. package/dist/domain/requests.js +33 -0
  21. package/dist/domain/requests.js.map +1 -1
  22. package/dist/domain/sponsored-calls.d.ts +74 -0
  23. package/dist/domain/sponsored-calls.js +156 -0
  24. package/dist/domain/sponsored-calls.js.map +1 -0
  25. package/dist/domain/yields.d.ts +121 -0
  26. package/dist/domain/yields.js +151 -0
  27. package/dist/domain/yields.js.map +1 -0
  28. package/dist/facade/calls.d.ts +59 -0
  29. package/dist/facade/calls.js +89 -0
  30. package/dist/facade/calls.js.map +1 -0
  31. package/dist/facade/chain-configs.js +5 -0
  32. package/dist/facade/chain-configs.js.map +1 -1
  33. package/dist/facade/flows.d.ts +24 -3
  34. package/dist/facade/flows.js +22 -2
  35. package/dist/facade/flows.js.map +1 -1
  36. package/dist/facade/index.d.ts +3 -1
  37. package/dist/facade/index.js +1 -0
  38. package/dist/facade/index.js.map +1 -1
  39. package/dist/facade/kasu.d.ts +8 -2
  40. package/dist/facade/kasu.js +17 -5
  41. package/dist/facade/kasu.js.map +1 -1
  42. package/dist/facade/strategies.d.ts +43 -2
  43. package/dist/facade/strategies.js +80 -4
  44. package/dist/facade/strategies.js.map +1 -1
  45. package/dist/facade/types.d.ts +18 -0
  46. package/dist/flows/batch-errors.d.ts +74 -0
  47. package/dist/flows/batch-errors.js +83 -0
  48. package/dist/flows/batch-errors.js.map +1 -0
  49. package/dist/flows/batch-submission.d.ts +223 -0
  50. package/dist/flows/batch-submission.js +321 -0
  51. package/dist/flows/batch-submission.js.map +1 -0
  52. package/dist/flows/deposit-flow.d.ts +13 -20
  53. package/dist/flows/deposit-flow.js +14 -25
  54. package/dist/flows/deposit-flow.js.map +1 -1
  55. package/dist/flows/index.d.ts +6 -2
  56. package/dist/flows/index.js +4 -2
  57. package/dist/flows/index.js.map +1 -1
  58. package/dist/flows/withdraw-flow.d.ts +95 -4
  59. package/dist/flows/withdraw-flow.js +107 -2
  60. package/dist/flows/withdraw-flow.js.map +1 -1
  61. package/dist/services/DataService/queries.js +1 -0
  62. package/dist/services/DataService/queries.js.map +1 -1
  63. package/dist/services/Portfolio/portfolio.js +11 -0
  64. package/dist/services/Portfolio/portfolio.js.map +1 -1
  65. package/dist/services/UserLending/user-lending.d.ts +13 -0
  66. package/dist/services/UserLending/user-lending.js +25 -4
  67. package/dist/services/UserLending/user-lending.js.map +1 -1
  68. package/package.json +1 -1
  69. package/src/domain/batch-proof.ts +576 -0
  70. package/src/domain/explorer-urls.ts +74 -0
  71. package/src/domain/fixed-term.ts +115 -0
  72. package/src/domain/index.ts +56 -0
  73. package/src/domain/partners.ts +35 -0
  74. package/src/domain/requests.ts +37 -0
  75. package/src/domain/sponsored-calls.ts +213 -0
  76. package/src/domain/yields.ts +189 -0
  77. package/src/facade/calls.ts +135 -0
  78. package/src/facade/chain-configs.ts +5 -0
  79. package/src/facade/flows.ts +23 -2
  80. package/src/facade/index.ts +3 -0
  81. package/src/facade/kasu.ts +31 -2
  82. package/src/facade/strategies.ts +98 -7
  83. package/src/facade/types.ts +19 -0
  84. package/src/flows/batch-errors.ts +118 -0
  85. package/src/flows/batch-submission.ts +511 -0
  86. package/src/flows/deposit-flow.ts +27 -33
  87. package/src/flows/index.ts +33 -2
  88. package/src/flows/withdraw-flow.ts +219 -10
  89. package/src/services/DataService/queries.ts +1 -0
  90. package/src/services/Portfolio/portfolio.ts +14 -0
  91. package/src/services/UserLending/user-lending.ts +38 -10
@@ -1,5 +1,5 @@
1
1
  import { formatUnits, parseUnits, formatEther, defaultAbiCoder, parseEther } from 'ethers/lib/utils';
2
- import { utils as utils$2, Contract, BigNumber, ethers, Signer, providers } from 'ethers';
2
+ import { utils as utils$2, Contract, BigNumber, ethers, constants, Signer, providers } from 'ethers';
3
3
 
4
4
  const defaultJsonSerializer = JSON;
5
5
 
@@ -2031,7 +2031,7 @@ function isSource(source) {
2031
2031
  * Given a GraphQL source, parses it into a Document.
2032
2032
  * Throws GraphQLError if a syntax error is encountered.
2033
2033
  */
2034
- function parse(source, options) {
2034
+ function parse$1(source, options) {
2035
2035
  const parser = new Parser(source, options);
2036
2036
  const document = parser.parseDocument();
2037
2037
  Object.defineProperty(document, 'tokenCount', {
@@ -4294,7 +4294,7 @@ const resolveRequestDocument = (document) => {
4294
4294
  if (typeof document === `string`) {
4295
4295
  let operationName = undefined;
4296
4296
  try {
4297
- const parsedDocument = parse(document);
4297
+ const parsedDocument = parse$1(document);
4298
4298
  operationName = extractOperationName(parsedDocument);
4299
4299
  }
4300
4300
  catch (err) {
@@ -15809,6 +15809,7 @@ const getPoolOverviewQuery = (ids) => gql `
15809
15809
  fixedTermDepositStatus
15810
15810
  fixedTermDepositAllowlist{
15811
15811
  id
15812
+ isAllowlisted
15812
15813
  user{
15813
15814
  id
15814
15815
  }
@@ -17238,10 +17239,32 @@ class UserLending {
17238
17239
  return yield this._lendingPoolManagerAbi.requestWithdrawal(lendingPool, tranche, amount);
17239
17240
  });
17240
17241
  }
17241
- requestWithdrawalInAsset(lendingPool, tranche, assetAmount) {
17242
+ /**
17243
+ * The tranche shares a withdrawal of `assetAmount` requests — the
17244
+ * tranche's own `convertToShares`, read now. `requestWithdrawalInAsset`
17245
+ * and the sponsored `kasu.calls.requestWithdrawal` both use this, so the
17246
+ * two paths cannot convert differently.
17247
+ */
17248
+ getWithdrawalSharesForAssets(tranche, assetAmount) {
17249
+ return __awaiter(this, void 0, void 0, function* () {
17250
+ const trancheContract = ILendingPoolTrancheAbi__factory.connect(tranche, this._signerOrProvider);
17251
+ return yield trancheContract.convertToShares(assetAmount);
17252
+ });
17253
+ }
17254
+ /**
17255
+ * Every tranche share `user` holds — what a max withdrawal requests
17256
+ * (`balanceOf(user)` on the tranche, read now). Shared by
17257
+ * `requestWithdrawalMax` and the sponsored `kasu.calls.requestWithdrawal`.
17258
+ */
17259
+ getWithdrawalSharesMax(tranche, user) {
17242
17260
  return __awaiter(this, void 0, void 0, function* () {
17243
17261
  const trancheContract = ILendingPoolTrancheAbi__factory.connect(tranche, this._signerOrProvider);
17244
- const shareAmount = yield trancheContract.convertToShares(assetAmount);
17262
+ return yield trancheContract['balanceOf(address)'](user);
17263
+ });
17264
+ }
17265
+ requestWithdrawalInAsset(lendingPool, tranche, assetAmount) {
17266
+ return __awaiter(this, void 0, void 0, function* () {
17267
+ const shareAmount = yield this.getWithdrawalSharesForAssets(tranche, assetAmount);
17245
17268
  return yield this._lendingPoolManagerAbi.requestWithdrawal(lendingPool, tranche, shareAmount);
17246
17269
  });
17247
17270
  }
@@ -17253,8 +17276,7 @@ class UserLending {
17253
17276
  }
17254
17277
  requestWithdrawalMax(lendingPool, tranche, user) {
17255
17278
  return __awaiter(this, void 0, void 0, function* () {
17256
- const trancheContract = ILendingPoolTrancheAbi__factory.connect(tranche, this._signerOrProvider);
17257
- const userShares = yield trancheContract['balanceOf(address)'](user);
17279
+ const userShares = yield this.getWithdrawalSharesMax(tranche, user);
17258
17280
  return yield this._lendingPoolManagerAbi.requestWithdrawal(lendingPool, tranche, userShares);
17259
17281
  });
17260
17282
  }
@@ -17829,6 +17851,17 @@ class Portfolio {
17829
17851
  weeklyYieldEarnings +=
17830
17852
  investedAmount * parseFloat(tranche.interestRate);
17831
17853
  for (const fixedDeposits of tranche.fixedLoans) {
17854
+ // A matured lock's principal is already inside
17855
+ // `investedAmount`: getPortfolioLendingData subtracts only
17856
+ // the LOCKED loans from the tranche balance. Its lifetime
17857
+ // yield is earned and stays counted (the tranche's own
17858
+ // lifetime had every lock's yield taken out), but its
17859
+ // amount must not be added to the investment, weekly or
17860
+ // APY sums a second time.
17861
+ if (!fixedDeposits.isLocked) {
17862
+ totalYieldEarned += parseFloat(fixedDeposits.yieldEarnings.lifetime);
17863
+ continue;
17864
+ }
17832
17865
  const ftdConfig = tranche.fixedTermConfig.find((ftd) => ftd.configId === fixedDeposits.configId);
17833
17866
  if (!ftdConfig)
17834
17867
  continue;
@@ -20460,7 +20493,7 @@ function toFormData$1(obj, formData, options) {
20460
20493
  *
20461
20494
  * @returns {string} The encoded string.
20462
20495
  */
20463
- function encode$1(str) {
20496
+ function encode$2(str) {
20464
20497
  const charMap = {
20465
20498
  '!': '%21',
20466
20499
  "'": '%27',
@@ -20496,8 +20529,8 @@ prototype.append = function append(name, value) {
20496
20529
 
20497
20530
  prototype.toString = function toString(encoder) {
20498
20531
  const _encode = encoder
20499
- ? (value) => encoder.call(this, value, encode$1)
20500
- : encode$1;
20532
+ ? (value) => encoder.call(this, value, encode$2)
20533
+ : encode$2;
20501
20534
 
20502
20535
  return this._pairs
20503
20536
  .map(function each(pair) {
@@ -20514,7 +20547,7 @@ prototype.toString = function toString(encoder) {
20514
20547
  *
20515
20548
  * @returns {string} The encoded value.
20516
20549
  */
20517
- function encode(val) {
20550
+ function encode$1(val) {
20518
20551
  return encodeURIComponent(val)
20519
20552
  .replace(/%3A/gi, ':')
20520
20553
  .replace(/%24/g, '$')
@@ -20546,7 +20579,7 @@ function buildURL(url, params, options) {
20546
20579
  // Read serializer options pollution-safely: own properties and methods on a
20547
20580
  // class/template prototype are honored, but values injected onto a polluted
20548
20581
  // Object.prototype are ignored.
20549
- const _encode = utils$1.getSafeProp(_options, 'encode') || encode;
20582
+ const _encode = utils$1.getSafeProp(_options, 'encode') || encode$1;
20550
20583
  const serializeFn = utils$1.getSafeProp(_options, 'serialize');
20551
20584
 
20552
20585
  let serializedParams;
@@ -21186,7 +21219,7 @@ let CanceledError$1 = class CanceledError extends AxiosError$1 {
21186
21219
  *
21187
21220
  * @returns {object} The response.
21188
21221
  */
21189
- function settle(resolve, reject, response) {
21222
+ function settle$1(resolve, reject, response) {
21190
21223
  const validateStatus = response.config.validateStatus;
21191
21224
  if (!response.status || !validateStatus || validateStatus(response.status)) {
21192
21225
  resolve(response);
@@ -21949,7 +21982,7 @@ var xhrAdapter = isXHRAdapterSupported &&
21949
21982
  request,
21950
21983
  };
21951
21984
 
21952
- settle(
21985
+ settle$1(
21953
21986
  function _resolve(value) {
21954
21987
  resolve(value);
21955
21988
  done();
@@ -23007,7 +23040,7 @@ const factory = (env) => {
23007
23040
  !isStreamResponse && unsubscribe && unsubscribe();
23008
23041
 
23009
23042
  return await new Promise((resolve, reject) => {
23010
- settle(resolve, reject, {
23043
+ settle$1(resolve, reject, {
23011
23044
  data: responseData,
23012
23045
  headers: AxiosHeaders$1.from(response.headers),
23013
23046
  status: response.status,
@@ -24183,6 +24216,238 @@ class SdkConfig {
24183
24216
  }
24184
24217
  }
24185
24218
 
24219
+ /**
24220
+ * The single-call `LendingPoolManager` requests a sponsored relay may carry,
24221
+ * encoded by the SDK so no application hand-builds calldata or hand-maps the
24222
+ * relay's `kind` for them.
24223
+ *
24224
+ * Every function here is PURE: addresses and ids in, `{ kind, call }` out.
24225
+ * Reading the chain (the shares a withdrawal is for) is the facade's job —
24226
+ * `kasu.calls` — and sending is the application's.
24227
+ *
24228
+ * Each call is `{ to, data }` and nothing else: no `value` (none of these
24229
+ * functions is payable), no `from` (the manager acts for `msg.sender`, the
24230
+ * lender's own account, which is why none of them takes a user argument).
24231
+ * `data` is ethers' ABI encoding, which is canonical by construction — the
24232
+ * relay re-encodes what it decodes and refuses anything that differs, so a
24233
+ * hand-tweaked blob never gets through, and an SDK-built one always matches.
24234
+ */
24235
+ /**
24236
+ * The relay's name for each shape. Carried beside the call so the app posts
24237
+ * `kind` from the SDK, never from a table of its own — a request declaring the
24238
+ * wrong kind is refused (`invalid_calls`), so a hand-mapped table that drifts
24239
+ * turns into a lender who cannot withdraw.
24240
+ */
24241
+ const SPONSORED_CALL_KINDS = [
24242
+ /** `LendingPoolManager.requestWithdrawal(pool, tranche, shares)`. */
24243
+ 'withdrawal',
24244
+ /** `LendingPoolManager.cancelDepositRequest(pool, dNftID)`. */
24245
+ 'cancel_deposit',
24246
+ /** `LendingPoolManager.cancelWithdrawalRequest(pool, wNftID)`. */
24247
+ 'cancel_withdrawal',
24248
+ /** `LendingPoolManager.requestFixedTermDepositWithdrawal(pool, lockId)`. */
24249
+ 'withdraw_at_expiry',
24250
+ ];
24251
+ /**
24252
+ * Widened to the plain `Interface`: the typechain one overloads
24253
+ * `encodeFunctionData` per literal name, and the name here comes from
24254
+ * `FUNCTION_OF`.
24255
+ */
24256
+ const LENDING_POOL_MANAGER$2 = ILendingPoolManagerAbi__factory.createInterface();
24257
+ /** The `LendingPoolManager` function behind each kind. */
24258
+ const FUNCTION_OF = Object.freeze({
24259
+ withdrawal: 'requestWithdrawal',
24260
+ cancel_deposit: 'cancelDepositRequest',
24261
+ cancel_withdrawal: 'cancelWithdrawalRequest',
24262
+ withdraw_at_expiry: 'requestFixedTermDepositWithdrawal',
24263
+ });
24264
+ /**
24265
+ * The 4-byte selector of each kind's function, DERIVED from the generated
24266
+ * `ILendingPoolManager` ABI — never typed in by hand, so an ABI change moves
24267
+ * it and the cross-check spec against the relay's own pins goes red.
24268
+ */
24269
+ const SPONSORED_CALL_SELECTORS = Object.freeze({
24270
+ withdrawal: LENDING_POOL_MANAGER$2.getSighash(FUNCTION_OF.withdrawal),
24271
+ cancel_deposit: LENDING_POOL_MANAGER$2.getSighash(FUNCTION_OF.cancel_deposit),
24272
+ cancel_withdrawal: LENDING_POOL_MANAGER$2.getSighash(FUNCTION_OF.cancel_withdrawal),
24273
+ withdraw_at_expiry: LENDING_POOL_MANAGER$2.getSighash(FUNCTION_OF.withdraw_at_expiry),
24274
+ });
24275
+ /**
24276
+ * What an encoder throws for an input it will not encode. `code` names the
24277
+ * argument; the message names no party, so a wallet-rejection classifier can
24278
+ * never read it as something the lender did. Nothing has been sent when this
24279
+ * is thrown.
24280
+ */
24281
+ class SponsoredCallInputError extends Error {
24282
+ constructor(code) {
24283
+ super(`Sponsored call input refused (${code})`);
24284
+ this.name = 'SponsoredCallInputError';
24285
+ this.code = code;
24286
+ Object.setPrototypeOf(this, SponsoredCallInputError.prototype);
24287
+ }
24288
+ }
24289
+ const ADDRESS_RE$2 = /^0x[0-9a-fA-F]{40}$/;
24290
+ function assertAddress(value, code) {
24291
+ if (typeof value !== 'string' || !ADDRESS_RE$2.test(value)) {
24292
+ throw new SponsoredCallInputError(code);
24293
+ }
24294
+ }
24295
+ /** A uint256 — an NFT or lock id. Zero is a valid id as far as encoding goes. */
24296
+ function toUint256(value, code) {
24297
+ let parsed;
24298
+ try {
24299
+ parsed = BigNumber.from(value);
24300
+ }
24301
+ catch (_a) {
24302
+ throw new SponsoredCallInputError(code);
24303
+ }
24304
+ if (parsed.isNegative() || parsed.gt(constants.MaxUint256)) {
24305
+ throw new SponsoredCallInputError(code);
24306
+ }
24307
+ return parsed;
24308
+ }
24309
+ function encode(kind, lendingPoolManager, args) {
24310
+ return {
24311
+ kind,
24312
+ call: {
24313
+ to: lendingPoolManager,
24314
+ data: LENDING_POOL_MANAGER$2.encodeFunctionData(FUNCTION_OF[kind], args),
24315
+ },
24316
+ };
24317
+ }
24318
+ /**
24319
+ * `requestWithdrawal(poolId, trancheId, shares)` — `withdrawal`.
24320
+ *
24321
+ * `shares` are tranche SHARES, not assets: `kasu.calls.requestWithdrawal`
24322
+ * converts an asset amount (or `'max'`) into them exactly as
24323
+ * `UserLending.requestWithdrawalInAsset` / `requestWithdrawalMax` do. Zero
24324
+ * shares is refused here: the contract reverts on it
24325
+ * (`RequestWithdrawSharesAreZero`) and the relay refuses it before that.
24326
+ */
24327
+ function encodeRequestWithdrawalCall(lendingPoolManager, poolId, trancheId, shares) {
24328
+ assertAddress(lendingPoolManager, 'lending-pool-manager');
24329
+ assertAddress(poolId, 'pool');
24330
+ assertAddress(trancheId, 'tranche');
24331
+ const amount = toUint256(shares, 'shares');
24332
+ if (amount.isZero())
24333
+ throw new SponsoredCallInputError('zero-shares');
24334
+ return encode('withdrawal', lendingPoolManager, [
24335
+ poolId,
24336
+ trancheId,
24337
+ amount,
24338
+ ]);
24339
+ }
24340
+ /** `cancelDepositRequest(poolId, dNftId)` — `cancel_deposit`. */
24341
+ function encodeCancelDepositCall(lendingPoolManager, poolId, dNftId) {
24342
+ assertAddress(lendingPoolManager, 'lending-pool-manager');
24343
+ assertAddress(poolId, 'pool');
24344
+ return encode('cancel_deposit', lendingPoolManager, [
24345
+ poolId,
24346
+ toUint256(dNftId, 'nft-id'),
24347
+ ]);
24348
+ }
24349
+ /** `cancelWithdrawalRequest(poolId, wNftId)` — `cancel_withdrawal`. */
24350
+ function encodeCancelWithdrawalCall(lendingPoolManager, poolId, wNftId) {
24351
+ assertAddress(lendingPoolManager, 'lending-pool-manager');
24352
+ assertAddress(poolId, 'pool');
24353
+ return encode('cancel_withdrawal', lendingPoolManager, [
24354
+ poolId,
24355
+ toUint256(wNftId, 'nft-id'),
24356
+ ]);
24357
+ }
24358
+ /**
24359
+ * `requestFixedTermDepositWithdrawal(poolId, fixedTermDepositId)` —
24360
+ * `withdraw_at_expiry`. The contract function takes the pool and the LOCK id
24361
+ * only (no tranche): the manager forwards to the fixed-term contract on the
24362
+ * lender's behalf, which knows the lock's tranche.
24363
+ */
24364
+ function encodeWithdrawAtExpiryCall(lendingPoolManager, poolId, fixedTermDepositId) {
24365
+ assertAddress(lendingPoolManager, 'lending-pool-manager');
24366
+ assertAddress(poolId, 'pool');
24367
+ return encode('withdraw_at_expiry', lendingPoolManager, [
24368
+ poolId,
24369
+ toUint256(fixedTermDepositId, 'lock-id'),
24370
+ ]);
24371
+ }
24372
+
24373
+ const ADDRESS_RE$1 = /^0x[0-9a-fA-F]{40}$/;
24374
+ /**
24375
+ * Encoded, ready-to-sponsor `LendingPoolManager` calls — read, then encode,
24376
+ * NEVER send. Each returns `{ kind, call }`: post `kind` and `[call]` to a
24377
+ * sponsored relay (or `submitSponsoredCall`), unchanged.
24378
+ *
24379
+ * ```ts
24380
+ * const { kind, call } = kasu.calls.cancelDepositRequest(poolId, dNftId);
24381
+ * ```
24382
+ *
24383
+ * `call.to` is always THIS chain's `LendingPoolManager`, from the chain
24384
+ * config — the same address the deposit flow approves and calls — so no app
24385
+ * supplies the one argument whose wrong value the relay refuses outright.
24386
+ *
24387
+ * Works on a read-only instance: the only chain access is the share read a
24388
+ * withdrawal needs, and nothing is signed or sent here.
24389
+ */
24390
+ class CallsFacade {
24391
+ constructor(_userLending, _lendingPoolManager) {
24392
+ this._userLending = _userLending;
24393
+ this._lendingPoolManager = _lendingPoolManager;
24394
+ }
24395
+ /**
24396
+ * `requestWithdrawal(pool, tranche, shares)` for an asset amount or
24397
+ * `'max'`, converted into shares exactly as the direct path does it
24398
+ * (`UserLending.requestWithdrawalInAsset` / `requestWithdrawalMax`): the
24399
+ * tranche's `convertToShares(amount)`, or the lender's whole
24400
+ * `balanceOf(userAddress)`, read now.
24401
+ *
24402
+ * Rejects with `SponsoredCallInputError` — before anything is sent — for
24403
+ * a non-positive amount, a malformed address, or a conversion that comes
24404
+ * to ZERO shares (an empty position, or dust below one share), which the
24405
+ * contract would revert and the relay refuses.
24406
+ */
24407
+ requestWithdrawal(params) {
24408
+ return __awaiter(this, void 0, void 0, function* () {
24409
+ const { poolId, trancheId, amount, userAddress } = params;
24410
+ if (typeof userAddress !== 'string' || !ADDRESS_RE$1.test(userAddress)) {
24411
+ throw new SponsoredCallInputError('user');
24412
+ }
24413
+ if (typeof poolId !== 'string' || !ADDRESS_RE$1.test(poolId)) {
24414
+ throw new SponsoredCallInputError('pool');
24415
+ }
24416
+ if (typeof trancheId !== 'string' || !ADDRESS_RE$1.test(trancheId)) {
24417
+ throw new SponsoredCallInputError('tranche');
24418
+ }
24419
+ let shares;
24420
+ if (amount === 'max') {
24421
+ // Lowercased, as `WithdrawFlow` hands `withdrawMax` the address.
24422
+ shares = yield this._userLending.getWithdrawalSharesMax(trancheId, userAddress.toLowerCase());
24423
+ }
24424
+ else {
24425
+ if (!BigNumber.isBigNumber(amount) || amount.lte(0)) {
24426
+ throw new SponsoredCallInputError('non-positive-amount');
24427
+ }
24428
+ shares = yield this._userLending.getWithdrawalSharesForAssets(trancheId, amount);
24429
+ }
24430
+ return encodeRequestWithdrawalCall(this._lendingPoolManager, poolId, trancheId, shares);
24431
+ });
24432
+ }
24433
+ /** `cancelDepositRequest(pool, dNftId)` — the pending deposit's NFT id. */
24434
+ cancelDepositRequest(poolId, dNftId) {
24435
+ return encodeCancelDepositCall(this._lendingPoolManager, poolId, dNftId);
24436
+ }
24437
+ /** `cancelWithdrawalRequest(pool, wNftId)` — the pending withdrawal's NFT id. */
24438
+ cancelWithdrawalRequest(poolId, wNftId) {
24439
+ return encodeCancelWithdrawalCall(this._lendingPoolManager, poolId, wNftId);
24440
+ }
24441
+ /**
24442
+ * `requestFixedTermDepositWithdrawal(pool, fixedTermDepositId)` — what
24443
+ * `UserLending.withdrawAtExpiry(pool, lockId)` sends: the pool and the
24444
+ * LOCK id, no tranche.
24445
+ */
24446
+ withdrawAtExpiry(poolId, fixedTermDepositId) {
24447
+ return encodeWithdrawAtExpiryCall(this._lendingPoolManager, poolId, fixedTermDepositId);
24448
+ }
24449
+ }
24450
+
24186
24451
  /**
24187
24452
  * Browser-viable XDC RPCs only. `rpc.xdc.org` / `erpc.xdc.org` answer OPTIONS
24188
24453
  * without `access-control-allow-origin`, so a browser call hangs on preflight
@@ -24240,6 +24505,7 @@ const CHAIN_CONFIGS = {
24240
24505
  'https://base-rpc.publicnode.com',
24241
24506
  'https://mainnet.base.org',
24242
24507
  ],
24508
+ blockExplorerUrl: 'https://basescan.org',
24243
24509
  },
24244
24510
  xdc: {
24245
24511
  chainId: 50,
@@ -24275,6 +24541,7 @@ const CHAIN_CONFIGS = {
24275
24541
  currencyCode: 'AUD',
24276
24542
  },
24277
24543
  rpcUrls: XDC_RPC_URLS,
24544
+ blockExplorerUrl: 'https://xdcscan.com',
24278
24545
  },
24279
24546
  'xdc-usdc': {
24280
24547
  chainId: 50,
@@ -24306,6 +24573,7 @@ const CHAIN_CONFIGS = {
24306
24573
  currencyCode: 'USD',
24307
24574
  },
24308
24575
  rpcUrls: XDC_RPC_URLS,
24576
+ blockExplorerUrl: 'https://xdcscan.com',
24309
24577
  },
24310
24578
  plume: {
24311
24579
  chainId: 98866,
@@ -24346,6 +24614,8 @@ const CHAIN_CONFIGS = {
24346
24614
  // pointing at an endpoint nobody maintains. Pass your own
24347
24615
  // `signerOrProvider` to read the frozen history.
24348
24616
  rpcUrls: [],
24617
+ // The explorer still serves the frozen history.
24618
+ blockExplorerUrl: 'https://explorer.plume.org',
24349
24619
  retired: true,
24350
24620
  },
24351
24621
  };
@@ -24465,114 +24735,1070 @@ class DepositsFacade {
24465
24735
  }
24466
24736
 
24467
24737
  /**
24468
- * The tiny observable-state kernel both flows are built on.
24738
+ * The errors an atomic-batch submitter throws, in one module both
24739
+ * `DepositFlow` and `submitBatch` import — so `instanceof` is one class
24740
+ * whichever of them a consumer imports it through.
24741
+ */
24742
+ /**
24743
+ * What a `depositBatch` port throws when it KNOWS the batch reverted — from a
24744
+ * simulation before sending, from the submitter refusing to broadcast a batch
24745
+ * that would revert, or from a mined receipt whose user operation reverted.
24746
+ * The run ends `{ step: 'request', reason: 'reverted', revertError }`.
24469
24747
  *
24470
- * Deliberately not a framework store: no React, no signals, no dependency.
24471
- * A flow is a long asynchronous run that has to publish every transition to
24472
- * whatever is rendering it, and has to be able to abandon a run that the
24473
- * consumer walked away from. That is the whole requirement, and it is 60 lines.
24748
+ * `revertError` is the custom error the contract named (`'ClearingIsPending'`,
24749
+ * `'UserNotKycd'`, …), exactly as the ABI declares it. Pass `''` when the
24750
+ * batch is known to revert but not why: the consumer then renders its generic
24751
+ * `reverted` copy, which is what an unknown name gets anyway.
24474
24752
  *
24475
- * The `generation` counter is the abandonment mechanism. `reset()` bumps it;
24476
- * an in-flight run captured the previous value, so every later `patch` it
24477
- * attempts is dropped and every `isCurrent` checkpoint tells it to stop. It is
24478
- * what makes `reset()` safe to call mid-flight — the run cannot resurrect a
24479
- * dead state or, worse, drive the UI back into a phase the consumer has left.
24753
+ * A revert is DEFINITIVE — nothing was deposited. Throw it only when that is
24754
+ * known; anything less certain is a `BatchPendingError`.
24480
24755
  */
24481
- class FlowStore {
24482
- constructor(_initial) {
24483
- this._initial = _initial;
24484
- this._generation = 0;
24485
- this._listeners = new Set();
24486
- this._state = _initial;
24487
- }
24488
- get state() {
24489
- return this._state;
24490
- }
24491
- /** The run token an in-flight pipeline captures at `start()`. */
24492
- get generation() {
24493
- return this._generation;
24494
- }
24495
- /** False once `reset()` (or a later `start()`) has superseded `token`. */
24496
- isCurrent(token) {
24497
- return this._generation === token;
24498
- }
24499
- /**
24500
- * Subscribe to every transition. Returns the unsubscribe function; calling
24501
- * it twice is harmless.
24502
- *
24503
- * Listeners are NOT called on subscribe — read `state` for the current
24504
- * value. A listener that throws is not allowed to break the pipeline or
24505
- * starve the listeners registered after it, so throws are swallowed.
24506
- */
24507
- subscribe(listener) {
24508
- this._listeners.add(listener);
24509
- return () => {
24510
- this._listeners.delete(listener);
24511
- };
24756
+ class BatchRevertedError extends Error {
24757
+ constructor(revertError, message) {
24758
+ super(message !== null && message !== void 0 ? message : `DepositFlow: the batch reverted${revertError ? ` with ${revertError}` : ''}`);
24759
+ this.name = 'BatchRevertedError';
24760
+ this.revertError = revertError;
24761
+ // Keeps `instanceof` honest under a down-levelled build.
24762
+ Object.setPrototypeOf(this, BatchRevertedError.prototype);
24512
24763
  }
24513
- /**
24514
- * Merge `next` into the state and notify, unless `token` names a
24515
- * superseded run.
24516
- */
24517
- patch(next, token) {
24518
- if (token !== undefined && !this.isCurrent(token))
24519
- return;
24520
- this._state = Object.assign(Object.assign({}, this._state), next);
24521
- for (const listener of [...this._listeners]) {
24522
- try {
24523
- listener(this._state);
24524
- }
24525
- catch (_a) {
24526
- // A consumer's rendering fault is its own problem; the pipeline
24527
- // owns real money and keeps going.
24528
- }
24764
+ }
24765
+ /**
24766
+ * Every non-revert failure of a batch submission, carrying its outcome and a
24767
+ * short code (`'status_timeout'`, `'receipt_unavailable'`, …).
24768
+ *
24769
+ * The message is fixed and names no party: the wallet-error classifier reads a
24770
+ * thrown error's text for a USER rejection, and a relay outage worded "…wallet
24771
+ * refused…" must never reach the lender as something they did.
24772
+ *
24773
+ * `outcome` has NO default. `failed` is the one outcome that invites a second
24774
+ * deposit, so a port author has to spell it out — a `throw new
24775
+ * BatchSubmissionError(code)` written in a catch block after the POST left
24776
+ * must not quietly become "try again".
24777
+ */
24778
+ class BatchSubmissionError extends Error {
24779
+ constructor(code, outcome, options) {
24780
+ super(`Batch submission ${outcome} (${code})`);
24781
+ this.name = 'BatchSubmissionError';
24782
+ this.code = code;
24783
+ this.outcome = outcome;
24784
+ // Set by hand: `Error`'s own `cause` option is ES2022.
24785
+ if (options && 'cause' in options) {
24786
+ Object.defineProperty(this, 'cause', {
24787
+ value: options.cause,
24788
+ enumerable: false,
24789
+ writable: true,
24790
+ configurable: true,
24791
+ });
24529
24792
  }
24793
+ Object.setPrototypeOf(this, BatchSubmissionError.prototype);
24530
24794
  }
24531
- /**
24532
- * Back to the initial state, abandoning any in-flight run.
24533
- *
24534
- * Subscribers are kept: the consumer that was watching the last run is the
24535
- * one that will watch the next.
24536
- */
24537
- reset() {
24538
- this._generation += 1;
24539
- this.patch(this._initial);
24540
- }
24541
- /** Claim the next run token. Any earlier run is abandoned. */
24542
- beginRun() {
24543
- this._generation += 1;
24544
- return this._generation;
24795
+ }
24796
+ /**
24797
+ * The ambiguous outcome: the batch may have landed. An instance of
24798
+ * `BatchSubmissionError` with `outcome: 'pending'`.
24799
+ */
24800
+ class BatchPendingError extends BatchSubmissionError {
24801
+ constructor(code, options) {
24802
+ super(code, 'pending', options);
24803
+ this.name = 'BatchPendingError';
24804
+ Object.setPrototypeOf(this, BatchPendingError.prototype);
24545
24805
  }
24546
24806
  }
24807
+ /** The outcome a thrown error stands for, or `null` if it is not a submission error. */
24808
+ function batchOutcomeOf(err) {
24809
+ return err instanceof BatchSubmissionError ? err.outcome : null;
24810
+ }
24811
+ /**
24812
+ * Nothing was deposited and nothing can be: a known revert, or a definitive
24813
+ * `failed` refusal. Everything else — including an error this SDK did not
24814
+ * produce — is NOT proof of that, and a consumer must not invite a retry on it.
24815
+ */
24816
+ function isDefinitivelyNotSubmitted(err) {
24817
+ return (err instanceof BatchRevertedError || batchOutcomeOf(err) === 'failed');
24818
+ }
24547
24819
 
24820
+ // ---------------------------------------------------------------------------
24821
+ // The selector tables
24822
+ // ---------------------------------------------------------------------------
24548
24823
  /**
24549
- * The run lifecycle both flows share: one run at a time, every transition
24550
- * published, and a `reset()` that is safe to call in the middle of one.
24551
- *
24552
- * `DepositFlow` and `WithdrawFlow` each carried their own copy of this — the
24553
- * re-entrancy guard, the `try/finally` around the run, `state`, `isRunning`,
24554
- * `subscribe`, `reset`. Two copies of a guard is two places for a guard to be
24555
- * wrong, on the money path, which is the same reason the pipelines themselves
24556
- * moved out of the applications.
24557
- *
24558
- * ## The guard and the run token are separate, and both matter
24824
+ * OpenZeppelin v5's ERC-20 errors. They are not in either Kasu ABI because no
24825
+ * Kasu contract declares them — they arrive from the TOKEN, through the
24826
+ * manager's `transferFrom`, and they are the modern spelling of the string
24827
+ * reverts below.
24828
+ */
24829
+ const ERC20_ERROR_ABI = [
24830
+ 'error ERC20InsufficientBalance(address sender, uint256 balance, uint256 needed)',
24831
+ 'error ERC20InsufficientAllowance(address spender, uint256 allowance, uint256 needed)',
24832
+ ];
24833
+ /**
24834
+ * The protocol errors that mean the ERC-20 transfer failed rather than the
24835
+ * protocol refusing.
24559
24836
  *
24560
- * `_runToken` is the re-entrancy guard: claimed synchronously, before the
24561
- * first `await`, so a double tap cannot launch two pipelines. The store's
24562
- * `generation` is the abandonment mechanism: an in-flight run holds the value
24563
- * it claimed, and every `patch` and `isCurrent` checkpoint it makes after
24564
- * `reset()` is dropped.
24837
+ * `SafeERC20FailedOperation` is OpenZeppelin's wrapper around a token call
24838
+ * that returned false or reverted, which on this path IS the `transferFrom`;
24839
+ * `AddressInsufficientBalance` is its native-value sibling. Both belong with
24840
+ * the balance case, not with the pool conditions — a lender who sees one of
24841
+ * them really does need to fund or re-approve.
24842
+ */
24843
+ const ERC20_FAMILY_NAMES = new Set([
24844
+ 'AddressInsufficientBalance',
24845
+ 'ERC20InsufficientAllowance',
24846
+ 'ERC20InsufficientBalance',
24847
+ 'SafeERC20FailedOperation',
24848
+ ]);
24849
+ /** Solidity's `Error(string)`, the selector every `require` reason arrives under. */
24850
+ const STRING_REVERT_SELECTOR = '0x08c379a0';
24851
+ /**
24852
+ * The `require` reasons that are a balance or an allowance, across the token
24853
+ * implementations in circulation: OpenZeppelin v4's sentences, and the
24854
+ * shorthand the minimal implementations use.
24855
+ */
24856
+ const ERC20_REASON_PATTERNS = [
24857
+ /exceeds\s+balance/,
24858
+ /exceeds\s+allowance/,
24859
+ /insufficient\s+balance/,
24860
+ /insufficient\s+allowance/,
24861
+ /transfer_from_failed/,
24862
+ /transferfrom\s+failed/,
24863
+ /low-level\s+call\s+failed/,
24864
+ ];
24865
+ /**
24866
+ * selector → error name, built once on first use.
24565
24867
  *
24566
- * `reset()` releases the guard IMMEDIATELY rather than waiting for the
24567
- * abandoned run to notice. It has to: a run parked on a wallet prompt that
24568
- * never answers is a promise that may never settle, and a consumer that has
24569
- * left that screen must be able to start a new run in the same tick. The
24570
- * abandoned run's `finally` therefore releases the guard only if it still
24571
- * holds it — by then a NEW run may own it, and clearing it there would let a
24572
- * third run start alongside the second.
24868
+ * Lazily, because a consumer that never sees a revert should not pay for three
24869
+ * `Interface` constructions at import. The two Kasu tables are the GENERATED
24870
+ * typechain ABIs, so this cannot drift from the contracts: regenerate the ABIs
24871
+ * and the new errors are recognised with no edit here.
24573
24872
  */
24574
- class Flow {
24575
- constructor(initial) {
24873
+ let selectorTable = null;
24874
+ function selectors() {
24875
+ if (selectorTable)
24876
+ return selectorTable;
24877
+ const table = new Map();
24878
+ const interfaces = [
24879
+ // The manager the deposit is called against, and the allow list it
24880
+ // delegates the KYC and block-window checks to.
24881
+ ILendingPoolManagerAbi__factory.createInterface(),
24882
+ IKasuAllowListAbi__factory.createInterface(),
24883
+ new ethers.utils.Interface(ERC20_ERROR_ABI),
24884
+ ];
24885
+ for (const iface of interfaces) {
24886
+ for (const fragment of Object.values(iface.errors)) {
24887
+ // First writer wins: the same name declared by both ABIs is the
24888
+ // same selector, so the duplicate would be a no-op anyway.
24889
+ const sighash = iface.getSighash(fragment);
24890
+ if (!table.has(sighash))
24891
+ table.set(sighash, fragment.name);
24892
+ }
24893
+ }
24894
+ selectorTable = table;
24895
+ return table;
24896
+ }
24897
+ // ---------------------------------------------------------------------------
24898
+ // Getting the bytes out of the envelope
24899
+ // ---------------------------------------------------------------------------
24900
+ /**
24901
+ * How deep to look for the revert data. Providers nest it differently —
24902
+ * ethers' own error wraps the provider's, which wraps the RPC body — and a
24903
+ * bound is what keeps a self-referencing error object from being walked
24904
+ * forever.
24905
+ */
24906
+ const MAX_DEPTH = 6;
24907
+ /** The keys ethers and the providers under it put a wrapped error on. */
24908
+ const NESTED_KEYS = [
24909
+ 'data',
24910
+ 'error',
24911
+ 'originalError',
24912
+ 'info',
24913
+ 'cause',
24914
+ 'body',
24915
+ ];
24916
+ /** At least a 4-byte selector, and a whole number of bytes. */
24917
+ function isRevertData(value) {
24918
+ return (value.length >= 10 &&
24919
+ value.length % 2 === 0 &&
24920
+ /^0x[0-9a-fA-F]+$/.test(value));
24921
+ }
24922
+ /**
24923
+ * Find the revert data on an error, wherever the provider left it.
24924
+ *
24925
+ * ethers v5 raises `UNPREDICTABLE_GAS_LIMIT` with the provider's own error
24926
+ * attached, and each provider nests the bytes one layer differently:
24927
+ * `error.data`, `error.error.data`, `error.data.originalError.data`, or only
24928
+ * inside the JSON `body` of a `SERVER_ERROR`. Rather than enumerate the
24929
+ * products, walk a fixed set of keys to a fixed depth and take the first value
24930
+ * that is shaped like revert data.
24931
+ */
24932
+ function extractRevertData(err) {
24933
+ return walk(err, 0);
24934
+ }
24935
+ function walk(node, depth) {
24936
+ if (node == null || depth > MAX_DEPTH)
24937
+ return null;
24938
+ if (typeof node === 'string') {
24939
+ if (isRevertData(node))
24940
+ return node;
24941
+ // A `SERVER_ERROR`'s body: the RPC's JSON reply, verbatim.
24942
+ if (!node.startsWith('{'))
24943
+ return null;
24944
+ try {
24945
+ return walk(JSON.parse(node), depth + 1);
24946
+ }
24947
+ catch (_a) {
24948
+ return null;
24949
+ }
24950
+ }
24951
+ if (typeof node !== 'object')
24952
+ return null;
24953
+ const record = node;
24954
+ for (const key of NESTED_KEYS) {
24955
+ const found = walk(record[key], depth + 1);
24956
+ if (found)
24957
+ return found;
24958
+ }
24959
+ return null;
24960
+ }
24961
+ // ---------------------------------------------------------------------------
24962
+ // Decoding
24963
+ // ---------------------------------------------------------------------------
24964
+ /**
24965
+ * Decode a failed call's revert into the error the contract named, or `null`
24966
+ * when there is nothing recognisable to name.
24967
+ *
24968
+ * `null` is the honest answer for an unknown selector, an unrelated `require`
24969
+ * string, or an error with no revert data at all — the caller keeps whatever
24970
+ * it did before rather than inventing a diagnosis from bytes it cannot read.
24971
+ */
24972
+ function decodeRevert(err) {
24973
+ const data = extractRevertData(err);
24974
+ if (!data)
24975
+ return null;
24976
+ const selector = data.slice(0, 10).toLowerCase();
24977
+ if (selector === STRING_REVERT_SELECTOR) {
24978
+ const reason = decodeStringRevert(data);
24979
+ if (reason === null)
24980
+ return null;
24981
+ return isErc20Reason(reason)
24982
+ ? { name: 'Error', family: 'erc20', reason }
24983
+ : null;
24984
+ }
24985
+ const name = selectors().get(selector);
24986
+ if (!name)
24987
+ return null;
24988
+ return {
24989
+ name,
24990
+ family: ERC20_FAMILY_NAMES.has(name) ? 'erc20' : 'protocol',
24991
+ };
24992
+ }
24993
+ function decodeStringRevert(data) {
24994
+ try {
24995
+ const [reason] = ethers.utils.defaultAbiCoder.decode(['string'], `0x${data.slice(10)}`);
24996
+ return reason;
24997
+ }
24998
+ catch (_a) {
24999
+ return null;
25000
+ }
25001
+ }
25002
+ function isErc20Reason(reason) {
25003
+ const lower = reason.toLowerCase();
25004
+ return ERC20_REASON_PATTERNS.some((pattern) => pattern.test(lower));
25005
+ }
25006
+
25007
+ /**
25008
+ * The money-path checks around an atomic, sponsored deposit batch — the
25009
+ * `approve + requestDepositWithKyc` pair `DepositFlow` hands its
25010
+ * `depositBatch` port.
25011
+ *
25012
+ * Two questions, both PURE:
25013
+ *
25014
+ * 1. Before anything is signed: is this batch exactly the deposit this run
25015
+ * asked for? (`validateDepositBatchCalls`)
25016
+ * 2. After it is mined: did THIS receipt deposit THIS amount, for THIS
25017
+ * lender, into THIS tranche? (`proveDepositInReceipt`)
25018
+ *
25019
+ * Neither knows a relay, a wallet vendor or a URL; the app's submitter calls
25020
+ * them.
25021
+ */
25022
+ /**
25023
+ * ERC-4337 EntryPoint v0.7 — the contract a bundler calls `handleOps` on. The
25024
+ * proof reads the user operation's outcome from ITS events, and from no other
25025
+ * contract's.
25026
+ */
25027
+ const ENTRY_POINT_V07 = '0x0000000071727De22E5E9d8BAf0edAc6f37da032';
25028
+ /** EntryPoint v0.7 events (eth-infinitism `IEntryPoint`). */
25029
+ const ENTRY_POINT_EVENTS = new utils$2.Interface([
25030
+ 'event UserOperationEvent(bytes32 indexed userOpHash, address indexed sender, address indexed paymaster, uint256 nonce, bool success, uint256 actualGasCost, uint256 actualGasUsed)',
25031
+ 'event UserOperationRevertReason(bytes32 indexed userOpHash, address indexed sender, uint256 nonce, bytes revertReason)',
25032
+ 'event BeforeExecution()',
25033
+ ]);
25034
+ /** kasu-contracts `IPendingPool` — emitted by the strategy's pending pool. */
25035
+ const PENDING_POOL_EVENTS = new utils$2.Interface([
25036
+ 'event DepositRequested(address indexed user, address indexed tranche, uint256 indexed dNftID, uint256 epochId, uint256 amount)',
25037
+ ]);
25038
+ const USER_OPERATION_EVENT = ENTRY_POINT_EVENTS.getEventTopic('UserOperationEvent');
25039
+ const USER_OPERATION_REVERT_REASON = ENTRY_POINT_EVENTS.getEventTopic('UserOperationRevertReason');
25040
+ const BEFORE_EXECUTION = ENTRY_POINT_EVENTS.getEventTopic('BeforeExecution');
25041
+ const DEPOSIT_REQUESTED = PENDING_POOL_EVENTS.getEventTopic('DepositRequested');
25042
+ const ERC20$1 = IERC20MetadataAbi__factory.createInterface();
25043
+ const LENDING_POOL_MANAGER$1 = ILendingPoolManagerAbi__factory.createInterface();
25044
+ const ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
25045
+ /**
25046
+ * Case-insensitive address equality; `false` for anything that is not a
25047
+ * 20-byte hex address. Checksums are not enforced — an all-lowercase address
25048
+ * is the subgraph's own spelling.
25049
+ */
25050
+ function sameAddress(a, b) {
25051
+ return (typeof a === 'string' &&
25052
+ typeof b === 'string' &&
25053
+ ADDRESS_RE.test(a) &&
25054
+ ADDRESS_RE.test(b) &&
25055
+ a.toLowerCase() === b.toLowerCase());
25056
+ }
25057
+ function isHexData(value) {
25058
+ return typeof value === 'string' && utils$2.isHexString(value);
25059
+ }
25060
+ /**
25061
+ * What `validateDepositBatchCalls` throws. `code` says which pin failed; the
25062
+ * message names no party, so a wallet-rejection classifier can never read it
25063
+ * as something the lender did.
25064
+ */
25065
+ class DepositBatchMismatchError extends Error {
25066
+ constructor(code) {
25067
+ super(`Deposit batch does not match the run (${code})`);
25068
+ this.name = 'DepositBatchMismatchError';
25069
+ this.code = code;
25070
+ Object.setPrototypeOf(this, DepositBatchMismatchError.prototype);
25071
+ }
25072
+ }
25073
+ function mismatch(code) {
25074
+ throw new DepositBatchMismatchError(code);
25075
+ }
25076
+ function isCallShape(call) {
25077
+ if (call === null || typeof call !== 'object')
25078
+ return false;
25079
+ const { to, data } = call;
25080
+ return typeof to === 'string' && ADDRESS_RE.test(to) && isHexData(data);
25081
+ }
25082
+ function isZeroValue(value) {
25083
+ if (value === undefined)
25084
+ return true;
25085
+ try {
25086
+ return BigNumber.from(value).isZero();
25087
+ }
25088
+ catch (_a) {
25089
+ return false;
25090
+ }
25091
+ }
25092
+ function sameAmount(a, b) {
25093
+ return BigNumber.isBigNumber(a) && a.eq(b);
25094
+ }
25095
+ /**
25096
+ * Decode `data` as a call to `name` on `iface`, and refuse it unless it is
25097
+ * the CANONICAL encoding of what it decodes to. The ABI decoder tolerates
25098
+ * trailing bytes and non-minimal offsets; a signature over bytes that decode
25099
+ * "the same" but are not the bytes the SDK produced is not a signature over
25100
+ * this run's deposit.
25101
+ */
25102
+ function decodeCanonical(iface, name, data, code) {
25103
+ let parsed;
25104
+ try {
25105
+ parsed = iface.parseTransaction({ data });
25106
+ }
25107
+ catch (_a) {
25108
+ return mismatch(code);
25109
+ }
25110
+ if (parsed.name !== name)
25111
+ return mismatch(code);
25112
+ let canonical;
25113
+ try {
25114
+ canonical = iface.encodeFunctionData(parsed.functionFragment, parsed.args);
25115
+ }
25116
+ catch (_b) {
25117
+ return mismatch(code);
25118
+ }
25119
+ if (canonical.toLowerCase() !== data.toLowerCase()) {
25120
+ return mismatch('non-canonical-encoding');
25121
+ }
25122
+ return parsed.args;
25123
+ }
25124
+ /**
25125
+ * Pin a deposit batch to the run that produced it — PURE; throws a
25126
+ * `DepositBatchMismatchError` on ANY deviation, before the lender's key signs
25127
+ * anything.
25128
+ *
25129
+ * The batch must be exactly two calls, in this order, with no native value:
25130
+ *
25131
+ * 0. `stableAsset.approve(spender, A)`;
25132
+ * 1. `spender.requestDepositWithKyc(poolId, trancheId, A, '0x', fixedTermConfigId, …)`;
25133
+ *
25134
+ * with `A` the run's own amount in BOTH — never more, never unlimited (house
25135
+ * rule: exact-amount approvals) — the run's own fixed-term config, each call
25136
+ * the canonical ABI encoding of its arguments, and no swap data. The deposit
25137
+ * data and KYC signature are not pinned here: they are signatures the
25138
+ * contract and the backend verify.
25139
+ *
25140
+ * This is the client's half; a relaying backend checks the same shape again.
25141
+ */
25142
+ function validateDepositBatchCalls(calls, expected) {
25143
+ const amount = BigNumber.from(expected.amount);
25144
+ if (amount.lte(0))
25145
+ mismatch('non-positive-amount');
25146
+ if (!Array.isArray(calls) || calls.length !== 2)
25147
+ mismatch('call-count');
25148
+ const [approveCall, depositCall] = calls;
25149
+ for (const call of [approveCall, depositCall]) {
25150
+ if (!isCallShape(call))
25151
+ mismatch('malformed-call');
25152
+ if (!isZeroValue(call.value))
25153
+ mismatch('native-value');
25154
+ }
25155
+ // 0 — the approval.
25156
+ if (!sameAddress(approveCall.to, expected.stableAsset)) {
25157
+ mismatch('approve-token');
25158
+ }
25159
+ const [approveSpender, approveAmount] = decodeCanonical(ERC20$1, 'approve', approveCall.data, 'approve-call');
25160
+ if (!sameAddress(approveSpender, expected.spender)) {
25161
+ mismatch('approve-spender');
25162
+ }
25163
+ // 1 — the deposit.
25164
+ if (!sameAddress(depositCall.to, expected.spender)) {
25165
+ mismatch('deposit-target');
25166
+ }
25167
+ const [pool, tranche, maxAmount, swapData, fixedTermConfigId] = decodeCanonical(LENDING_POOL_MANAGER$1, 'requestDepositWithKyc', depositCall.data, 'deposit-call');
25168
+ if (!sameAddress(pool, expected.poolId))
25169
+ mismatch('deposit-pool');
25170
+ if (!sameAddress(tranche, expected.trancheId))
25171
+ mismatch('deposit-tranche');
25172
+ if (swapData !== '0x')
25173
+ mismatch('swap-data');
25174
+ if (!sameAmount(fixedTermConfigId, BigNumber.from(expected.fixedTermConfigId))) {
25175
+ mismatch('fixed-term-config');
25176
+ }
25177
+ // EXACT amount, on both calls, equal to the run's.
25178
+ if (!sameAmount(approveAmount, amount))
25179
+ mismatch('approve-amount');
25180
+ if (!sameAmount(maxAmount, amount))
25181
+ mismatch('deposit-amount');
25182
+ return { tranche: tranche, amount };
25183
+ }
25184
+ /** A log's first topic, lowercased; `undefined` for a log without a topic array. */
25185
+ function topicOf(log) {
25186
+ var _a;
25187
+ return Array.isArray(log.topics)
25188
+ ? (_a = log.topics[0]) === null || _a === void 0 ? void 0 : _a.toLowerCase()
25189
+ : undefined;
25190
+ }
25191
+ function parse(iface, log) {
25192
+ try {
25193
+ return iface.parseLog({ topics: [...log.topics], data: log.data });
25194
+ }
25195
+ catch (_a) {
25196
+ return null;
25197
+ }
25198
+ }
25199
+ /**
25200
+ * Did THIS receipt deposit for THIS lender? — PURE.
25201
+ *
25202
+ * A sponsored batch lands as a bundler's `handleOps` transaction, and that
25203
+ * transaction's `status` is success even when the user operation inside it
25204
+ * reverted: the EntryPoint catches the revert, the account rolls back both
25205
+ * calls, nothing is deposited. So `status` decides NOTHING. What does:
25206
+ *
25207
+ * 1. The EntryPoint's `UserOperationEvent` for OUR operation — emitted BY
25208
+ * the EntryPoint, `sender` = lender, and `userOpHash` = the known hash
25209
+ * when there is one.
25210
+ * 2. Its `success` flag. `false` → `reverted`, naming the protocol error
25211
+ * from the matching `UserOperationRevertReason` when it decodes.
25212
+ * 3. A `DepositRequested` with `user` = lender, `tranche` = the run's and
25213
+ * `amount` = the run's, INSIDE our operation's execution: the logs after
25214
+ * the previous operation's event (or `BeforeExecution`, which precedes
25215
+ * the first) and before ours. A deposit from another operation in the
25216
+ * same bundle does not count, and neither does one emitted by a contract
25217
+ * that is not the pending pool, when the pending pool is known.
25218
+ *
25219
+ * Anything short of all three is `unproven`, which a consumer must treat as
25220
+ * PENDING — the money may have moved — and never as a failure.
25221
+ */
25222
+ function proveDepositInReceipt(receipt, expected) {
25223
+ // Typed as an array, but a receipt is foreign data: check before reading.
25224
+ if (!Array.isArray(receipt.logs)) {
25225
+ return { kind: 'unproven', reason: 'malformed-receipt' };
25226
+ }
25227
+ const amount = BigNumber.from(expected.amount);
25228
+ const op = locateUserOperation(receipt.logs, {
25229
+ sender: expected.lender,
25230
+ userOperationHash: expected.userOperationHash,
25231
+ entryPoint: expected.entryPoint,
25232
+ });
25233
+ if (op === null)
25234
+ return { kind: 'unproven', reason: 'no-user-operation' };
25235
+ if (op === 'ambiguous') {
25236
+ return { kind: 'unproven', reason: 'ambiguous-operation' };
25237
+ }
25238
+ if (!op.success) {
25239
+ return { kind: 'reverted', revertError: op.revertError() };
25240
+ }
25241
+ for (const log of op.window) {
25242
+ if (topicOf(log) !== DEPOSIT_REQUESTED)
25243
+ continue;
25244
+ if (expected.pendingPool !== undefined &&
25245
+ !sameAddress(log.address, expected.pendingPool)) {
25246
+ continue;
25247
+ }
25248
+ const decoded = parse(PENDING_POOL_EVENTS, log);
25249
+ if ((decoded === null || decoded === void 0 ? void 0 : decoded.name) !== 'DepositRequested')
25250
+ continue;
25251
+ if (!sameAddress(decoded.args.user, expected.lender))
25252
+ continue;
25253
+ if (!sameAddress(decoded.args.tranche, expected.tranche))
25254
+ continue;
25255
+ if (!sameAmount(decoded.args.amount, amount))
25256
+ continue;
25257
+ return {
25258
+ kind: 'deposited',
25259
+ dNftId: decoded.args.dNftID,
25260
+ blockNumber: receipt.blockNumber,
25261
+ };
25262
+ }
25263
+ return { kind: 'unproven', reason: 'no-deposit-requested' };
25264
+ }
25265
+ /**
25266
+ * Did THIS receipt execute the sender's user operation? — PURE. The
25267
+ * single-call sibling of `proveDepositInReceipt`, for a sponsored
25268
+ * `requestWithdrawal`, a cancel or a fixed-term withdrawal: the same
25269
+ * EntryPoint `UserOperationEvent` from the same contract, for the same
25270
+ * sender (and hash, when known), read the same way — the `handleOps`
25271
+ * transaction's own `status` decides NOTHING, because it is a success even
25272
+ * when the operation inside it reverted.
25273
+ *
25274
+ * `executed` needs the operation's `success` flag. A single-call operation
25275
+ * that succeeded executed its one call; there is no event to match beyond
25276
+ * that, so pass the `userOperationHash` whenever the submitter reports one —
25277
+ * without it, the sender's ONLY operation in the bundle is the one read, and a
25278
+ * bundle holding two of theirs is `ambiguous-operation` (pending), never the
25279
+ * first one's verdict: with one succeeded and one reverted, "executed" would
25280
+ * tell the lender their request is queued when it is not.
25281
+ */
25282
+ function proveUserOperationInReceipt(receipt, expected) {
25283
+ if (!Array.isArray(receipt.logs)) {
25284
+ return { kind: 'unproven', reason: 'malformed-receipt' };
25285
+ }
25286
+ const op = locateUserOperation(receipt.logs, expected);
25287
+ if (op === null)
25288
+ return { kind: 'unproven', reason: 'no-user-operation' };
25289
+ if (op === 'ambiguous') {
25290
+ return { kind: 'unproven', reason: 'ambiguous-operation' };
25291
+ }
25292
+ if (!op.success) {
25293
+ return { kind: 'reverted', revertError: op.revertError() };
25294
+ }
25295
+ return { kind: 'executed', blockNumber: receipt.blockNumber };
25296
+ }
25297
+ /**
25298
+ * The sender's user operation in a `handleOps` receipt — its `success` flag
25299
+ * and its execution window — `null` when the receipt does not show one, or
25300
+ * `'ambiguous'` when no hash was given and it shows more than one of theirs.
25301
+ *
25302
+ * - The `UserOperationEvent` must be emitted BY the EntryPoint, with
25303
+ * `sender` = the expected sender and, when one is known, `userOpHash` =
25304
+ * the expected hash. Without a hash the sender must have exactly ONE
25305
+ * operation in the bundle: a bundler may pack two of theirs (sequential
25306
+ * nonces, or parallel nonce keys), and reading the first would attribute
25307
+ * the other one's verdict to this call.
25308
+ * - The window is the logs after the previous operation's event (or
25309
+ * `BeforeExecution`, which precedes the first) and before ours: what our
25310
+ * operation's calls emitted, and nothing another operation in the same
25311
+ * bundle did.
25312
+ */
25313
+ function locateUserOperation(logs, expected) {
25314
+ var _a, _b, _c;
25315
+ const entryPoint = (_a = expected.entryPoint) !== null && _a !== void 0 ? _a : ENTRY_POINT_V07;
25316
+ const wantedHash = (_c = (_b = expected.userOperationHash) === null || _b === void 0 ? void 0 : _b.toLowerCase()) !== null && _c !== void 0 ? _c : null;
25317
+ const fromEntryPoint = (log) => sameAddress(log.address, entryPoint);
25318
+ let opIndex = -1;
25319
+ let success = false;
25320
+ for (let i = 0; i < logs.length; i += 1) {
25321
+ const log = logs[i];
25322
+ if (!fromEntryPoint(log) || topicOf(log) !== USER_OPERATION_EVENT) {
25323
+ continue;
25324
+ }
25325
+ const decoded = parse(ENTRY_POINT_EVENTS, log);
25326
+ if ((decoded === null || decoded === void 0 ? void 0 : decoded.name) !== 'UserOperationEvent')
25327
+ continue;
25328
+ const userOpHash = String(decoded.args.userOpHash).toLowerCase();
25329
+ if (wantedHash !== null && userOpHash !== wantedHash)
25330
+ continue;
25331
+ if (!sameAddress(decoded.args.sender, expected.sender))
25332
+ continue;
25333
+ // A second operation of this sender, and no hash to tell them apart.
25334
+ if (opIndex >= 0)
25335
+ return 'ambiguous';
25336
+ opIndex = i;
25337
+ success = decoded.args.success === true;
25338
+ // A hash names exactly one operation; without one, keep scanning so
25339
+ // a second operation of the sender is noticed.
25340
+ if (wantedHash !== null)
25341
+ break;
25342
+ }
25343
+ if (opIndex < 0)
25344
+ return null;
25345
+ // Our operation's execution window.
25346
+ let windowStart = 0;
25347
+ for (let i = opIndex - 1; i >= 0; i -= 1) {
25348
+ const log = logs[i];
25349
+ const topic = topicOf(log);
25350
+ if (fromEntryPoint(log) &&
25351
+ (topic === USER_OPERATION_EVENT || topic === BEFORE_EXECUTION)) {
25352
+ windowStart = i + 1;
25353
+ break;
25354
+ }
25355
+ }
25356
+ const window = logs.slice(windowStart, opIndex);
25357
+ return {
25358
+ success,
25359
+ window,
25360
+ revertError: () => revertErrorIn(window, expected.sender, fromEntryPoint),
25361
+ };
25362
+ }
25363
+ /**
25364
+ * The protocol error a reverted operation named, or `''`. Only a PROTOCOL
25365
+ * error is passed on: an `Error(string)` or an ERC-20 failure is not a name a
25366
+ * consumer has words for.
25367
+ */
25368
+ function revertErrorIn(window, lender, fromEntryPoint) {
25369
+ for (const log of window) {
25370
+ if (!fromEntryPoint(log))
25371
+ continue;
25372
+ // Through the same guard as the main loop: a malformed log beside the
25373
+ // revert reason must not turn a DEFINITIVE revert into "pending".
25374
+ if (topicOf(log) !== USER_OPERATION_REVERT_REASON)
25375
+ continue;
25376
+ const decoded = parse(ENTRY_POINT_EVENTS, log);
25377
+ if ((decoded === null || decoded === void 0 ? void 0 : decoded.name) !== 'UserOperationRevertReason')
25378
+ continue;
25379
+ if (!sameAddress(decoded.args.sender, lender))
25380
+ continue;
25381
+ const revert = decodeRevert(decoded.args.revertReason);
25382
+ return (revert === null || revert === void 0 ? void 0 : revert.family) === 'protocol' ? revert.name : '';
25383
+ }
25384
+ return '';
25385
+ }
25386
+
25387
+ /**
25388
+ * First read at once, then 3 s backing off ×1.5 to a 10 s ceiling, for up to
25389
+ * 10 minutes. Running out of time is PENDING: the batch was accepted and may
25390
+ * still land.
25391
+ */
25392
+ const DEFAULT_BATCH_POLLING_POLICY = Object.freeze({
25393
+ initialMs: 3000,
25394
+ maxMs: 10000,
25395
+ backoff: 1.5,
25396
+ timeoutMs: 10 * 60 * 1000,
25397
+ });
25398
+ function resolvePolicy(policy) {
25399
+ var _a, _b, _c, _d;
25400
+ const resolved = {
25401
+ initialMs: (_a = policy === null || policy === void 0 ? void 0 : policy.initialMs) !== null && _a !== void 0 ? _a : DEFAULT_BATCH_POLLING_POLICY.initialMs,
25402
+ maxMs: (_b = policy === null || policy === void 0 ? void 0 : policy.maxMs) !== null && _b !== void 0 ? _b : DEFAULT_BATCH_POLLING_POLICY.maxMs,
25403
+ backoff: (_c = policy === null || policy === void 0 ? void 0 : policy.backoff) !== null && _c !== void 0 ? _c : DEFAULT_BATCH_POLLING_POLICY.backoff,
25404
+ timeoutMs: (_d = policy === null || policy === void 0 ? void 0 : policy.timeoutMs) !== null && _d !== void 0 ? _d : DEFAULT_BATCH_POLLING_POLICY.timeoutMs,
25405
+ };
25406
+ const { initialMs, maxMs, backoff, timeoutMs } = resolved;
25407
+ if (!(Number.isFinite(initialMs) && initialMs > 0) ||
25408
+ !(Number.isFinite(maxMs) && maxMs >= initialMs) ||
25409
+ !(Number.isFinite(backoff) && backoff >= 1) ||
25410
+ !(Number.isFinite(timeoutMs) && timeoutMs > 0)) {
25411
+ throw new TypeError('submitBatch: invalid polling policy');
25412
+ }
25413
+ return resolved;
25414
+ }
25415
+ /**
25416
+ * Submit `calls` and return once the submitter ACCEPTED them; `wait()` then
25417
+ * polls and proves. Use it as the body of a `DepositFlow` `depositBatch`
25418
+ * port, after `validateDepositBatchCalls` and the app's own signing:
25419
+ *
25420
+ * ```ts
25421
+ * depositBatch: (calls) => {
25422
+ * const { tranche, amount } = validateDepositBatchCalls(calls, expected);
25423
+ * return submitBatch(
25424
+ * // `pendingPool`: the strategy's pending pool, so only ITS
25425
+ * // `DepositRequested` can prove the deposit.
25426
+ * { calls, expect: { lender, tranche, amount, pendingPool } },
25427
+ * { post, getStatus, getReceipt, now: Date.now, sleep },
25428
+ * );
25429
+ * },
25430
+ * ```
25431
+ *
25432
+ * | what happened | outcome |
25433
+ * | ---------------------------------------------------- | ---------------------------------------- |
25434
+ * | `post` throws `BatchRevertedError` | rethrown — reverted, nothing deposited |
25435
+ * | `post` throws `BatchSubmissionError` | rethrown with its own outcome |
25436
+ * | `post` throws anything else | `BatchPendingError('post_unknown')` |
25437
+ * | `post` resolves without a transaction id | `BatchPendingError('malformed_answer')` |
25438
+ * | status `reverted` | `BatchRevertedError('')` |
25439
+ * | status `unknown` | `BatchPendingError(code)` |
25440
+ * | polling runs out of time | `BatchPendingError('status_timeout')` |
25441
+ * | receipt unreadable | `BatchPendingError('receipt_unavailable')` |
25442
+ * | receipt: user operation reverted | `BatchRevertedError(revertError)` |
25443
+ * | receipt: this deposit not shown | `BatchPendingError('deposit_unproven:<reason>')` |
25444
+ * | receipt: this lender's deposit, tranche and amount | resolves `ProvenBatch` |
25445
+ *
25446
+ * Nothing on this path ever produces a `failed` outcome: by the time a batch
25447
+ * has been posted, only a revert proves that nothing was deposited.
25448
+ */
25449
+ function submitBatch(input, ports, policy) {
25450
+ return __awaiter(this, void 0, void 0, function* () {
25451
+ const resolved = resolvePolicy(policy);
25452
+ const transactionId = yield postOnce(() => ports.post(input.calls));
25453
+ let settled = null;
25454
+ return {
25455
+ transactionId,
25456
+ // Memoised: a second `wait()` reads the same outcome instead of
25457
+ // polling the submitter twice.
25458
+ wait: () => (settled !== null && settled !== void 0 ? settled : (settled = settle(transactionId, input.expect, ports, resolved))),
25459
+ };
25460
+ });
25461
+ }
25462
+ /**
25463
+ * The post, and what its answer means: the transaction id, or the error the
25464
+ * outcome table names. Shared by both submitters, so the two cannot come to
25465
+ * disagree about which throws are ambiguous.
25466
+ */
25467
+ function postOnce(post) {
25468
+ return __awaiter(this, void 0, void 0, function* () {
25469
+ let accepted;
25470
+ try {
25471
+ accepted = yield post();
25472
+ }
25473
+ catch (err) {
25474
+ if (err instanceof BatchRevertedError)
25475
+ throw err;
25476
+ if (err instanceof BatchSubmissionError)
25477
+ throw err;
25478
+ throw new BatchPendingError('post_unknown', { cause: err });
25479
+ }
25480
+ const transactionId = accepted === null || accepted === void 0 ? void 0 : accepted.transactionId;
25481
+ if (typeof transactionId !== 'string' || transactionId === '') {
25482
+ throw new BatchPendingError('malformed_answer');
25483
+ }
25484
+ return transactionId;
25485
+ });
25486
+ }
25487
+ function readStatus(ports, transactionId) {
25488
+ return __awaiter(this, void 0, void 0, function* () {
25489
+ try {
25490
+ return yield ports.getStatus(transactionId);
25491
+ }
25492
+ catch (_a) {
25493
+ return { kind: 'again' };
25494
+ }
25495
+ });
25496
+ }
25497
+ /** Poll until a mined hash, or a typed reason there will not be one. */
25498
+ function waitForMined(transactionId, ports, policy) {
25499
+ return __awaiter(this, void 0, void 0, function* () {
25500
+ var _a;
25501
+ const deadline = ports.now() + policy.timeoutMs;
25502
+ let userOperationHash = null;
25503
+ let delay = policy.initialMs;
25504
+ for (;;) {
25505
+ const read = yield readStatus(ports, transactionId);
25506
+ switch (read.kind) {
25507
+ case 'mined':
25508
+ if (typeof read.txHash !== 'string' || read.txHash === '') {
25509
+ break; // a malformed "mined" is not an answer; ask again
25510
+ }
25511
+ return {
25512
+ txHash: read.txHash,
25513
+ userOperationHash: (_a = read.userOperationHash) !== null && _a !== void 0 ? _a : userOperationHash,
25514
+ };
25515
+ case 'reverted':
25516
+ throw new BatchRevertedError('');
25517
+ case 'unknown':
25518
+ throw new BatchPendingError(read.code);
25519
+ case 'again':
25520
+ if (read.userOperationHash) {
25521
+ userOperationHash = read.userOperationHash;
25522
+ }
25523
+ break;
25524
+ }
25525
+ if (ports.now() + delay > deadline) {
25526
+ throw new BatchPendingError('status_timeout');
25527
+ }
25528
+ yield ports.sleep(delay);
25529
+ delay = Math.min(policy.maxMs, Math.round(delay * policy.backoff));
25530
+ }
25531
+ });
25532
+ }
25533
+ /** Poll to a mined hash and read its receipt; a receipt read that throws is PENDING. */
25534
+ function minedReceipt(transactionId, ports, policy) {
25535
+ return __awaiter(this, void 0, void 0, function* () {
25536
+ const mined = yield waitForMined(transactionId, ports, policy);
25537
+ try {
25538
+ return {
25539
+ receipt: yield ports.getReceipt(mined.txHash),
25540
+ txHash: mined.txHash,
25541
+ userOperationHash: mined.userOperationHash,
25542
+ };
25543
+ }
25544
+ catch (err) {
25545
+ throw new BatchPendingError('receipt_unavailable', { cause: err });
25546
+ }
25547
+ });
25548
+ }
25549
+ function settle(transactionId, expect, ports, policy) {
25550
+ return __awaiter(this, void 0, void 0, function* () {
25551
+ const mined = yield minedReceipt(transactionId, ports, policy);
25552
+ let proof;
25553
+ try {
25554
+ proof = proveDepositInReceipt(mined.receipt, Object.assign(Object.assign({}, expect), { userOperationHash: mined.userOperationHash }));
25555
+ }
25556
+ catch (err) {
25557
+ // A receipt the proof cannot even read proves nothing either way.
25558
+ throw new BatchPendingError('deposit_unproven', { cause: err });
25559
+ }
25560
+ if (proof.kind === 'reverted')
25561
+ throw new BatchRevertedError(proof.revertError);
25562
+ if (proof.kind === 'unproven') {
25563
+ // The proof's reason rides in the code: "no-user-operation" (the hash
25564
+ // is not this lender's operation at all) and "no-deposit-requested"
25565
+ // (the operation ran and shows no deposit) are the two things an
25566
+ // operator reading the alert needs to tell apart first.
25567
+ throw new BatchPendingError(`deposit_unproven:${proof.reason}`);
25568
+ }
25569
+ return { proof, receipt: mined.receipt, txHash: mined.txHash };
25570
+ });
25571
+ }
25572
+ /**
25573
+ * What became of an earlier submission that ended PENDING — ONE status read,
25574
+ * no polling — for an app's guard against lending twice:
25575
+ *
25576
+ * - `deposited` — mined, and the receipt proves the deposit;
25577
+ * - `not-deposited` — definitively reverted (vendor status or the user
25578
+ * operation's own flag);
25579
+ * - `unknown` — anything else, including a read or receipt that failed.
25580
+ *
25581
+ * Only `not-deposited` may let a new lend proceed unprompted; `deposited`
25582
+ * means the lender is told the earlier one landed (`already-submitted`).
25583
+ */
25584
+ function checkBatchOutcome(transactionId, expect, ports) {
25585
+ return __awaiter(this, void 0, void 0, function* () {
25586
+ var _a;
25587
+ let read;
25588
+ try {
25589
+ read = yield ports.getStatus(transactionId);
25590
+ }
25591
+ catch (_b) {
25592
+ return 'unknown';
25593
+ }
25594
+ if (read.kind === 'reverted')
25595
+ return 'not-deposited';
25596
+ if (read.kind !== 'mined' || typeof read.txHash !== 'string') {
25597
+ return 'unknown';
25598
+ }
25599
+ try {
25600
+ const receipt = yield ports.getReceipt(read.txHash);
25601
+ const proof = proveDepositInReceipt(receipt, Object.assign(Object.assign({}, expect), { userOperationHash: (_a = read.userOperationHash) !== null && _a !== void 0 ? _a : null }));
25602
+ if (proof.kind === 'deposited')
25603
+ return 'deposited';
25604
+ if (proof.kind === 'reverted')
25605
+ return 'not-deposited';
25606
+ return 'unknown';
25607
+ }
25608
+ catch (_c) {
25609
+ return 'unknown';
25610
+ }
25611
+ });
25612
+ }
25613
+ /**
25614
+ * `submitBatch` for ONE sponsored `LendingPoolManager` call — the body of a
25615
+ * `WithdrawFlow` `submitCall` port, and of an app's cancel or
25616
+ * withdraw-at-expiry action. Same post, same polling policy, same outcome
25617
+ * table; the proof is `proveUserOperationInReceipt` (the sender's operation
25618
+ * succeeded) instead of the deposit proof, and an unproven receipt is
25619
+ * `call_unproven:<reason>`.
25620
+ *
25621
+ * ```ts
25622
+ * submitCall: (call) =>
25623
+ * submitSponsoredCall(
25624
+ * { call, expect: { sender: lender } },
25625
+ * { post: (c) => postToMyRelay(c.kind, [c.call]), getStatus, getReceipt, now, sleep },
25626
+ * ),
25627
+ * ```
25628
+ *
25629
+ * The call is checked against its kind before anything is posted — a known
25630
+ * kind, exactly `{ to, data }`, and `data` opening with that kind's selector —
25631
+ * and a mismatch is `BatchSubmissionError('call_mismatch', 'failed')`: nothing
25632
+ * was sent. Build calls with `kasu.calls`, and this never fires.
25633
+ *
25634
+ * As on the deposit path, nothing after the post ever produces `failed`.
25635
+ */
25636
+ function submitSponsoredCall(input, ports, policy) {
25637
+ return __awaiter(this, void 0, void 0, function* () {
25638
+ const resolved = resolvePolicy(policy);
25639
+ if (!isWellFormedSponsoredCall(input.call)) {
25640
+ throw new BatchSubmissionError('call_mismatch', 'failed');
25641
+ }
25642
+ const transactionId = yield postOnce(() => ports.post(input.call));
25643
+ let settled = null;
25644
+ return {
25645
+ transactionId,
25646
+ wait: () => (settled !== null && settled !== void 0 ? settled : (settled = settleCall(transactionId, input.expect, ports, resolved))),
25647
+ };
25648
+ });
25649
+ }
25650
+ function isWellFormedSponsoredCall(value) {
25651
+ if (value === null || typeof value !== 'object')
25652
+ return false;
25653
+ const { kind, call } = value;
25654
+ if (typeof kind !== 'string' ||
25655
+ !SPONSORED_CALL_KINDS.includes(kind)) {
25656
+ return false;
25657
+ }
25658
+ if (call === null || typeof call !== 'object')
25659
+ return false;
25660
+ const keys = Object.keys(call);
25661
+ if (keys.length !== 2 || !keys.includes('to') || !keys.includes('data')) {
25662
+ return false;
25663
+ }
25664
+ const { to, data } = call;
25665
+ return (typeof to === 'string' &&
25666
+ /^0x[0-9a-fA-F]{40}$/.test(to) &&
25667
+ typeof data === 'string' &&
25668
+ /^0x(?:[0-9a-fA-F]{2})*$/.test(data) &&
25669
+ data
25670
+ .toLowerCase()
25671
+ .startsWith(SPONSORED_CALL_SELECTORS[kind]));
25672
+ }
25673
+ function settleCall(transactionId, expect, ports, policy) {
25674
+ return __awaiter(this, void 0, void 0, function* () {
25675
+ const mined = yield minedReceipt(transactionId, ports, policy);
25676
+ let proof;
25677
+ try {
25678
+ proof = proveUserOperationInReceipt(mined.receipt, Object.assign(Object.assign({}, expect), { userOperationHash: mined.userOperationHash }));
25679
+ }
25680
+ catch (err) {
25681
+ throw new BatchPendingError('call_unproven', { cause: err });
25682
+ }
25683
+ if (proof.kind === 'reverted') {
25684
+ throw new BatchRevertedError(proof.revertError);
25685
+ }
25686
+ if (proof.kind === 'unproven') {
25687
+ throw new BatchPendingError(`call_unproven:${proof.reason}`);
25688
+ }
25689
+ return { proof, receipt: mined.receipt, txHash: mined.txHash };
25690
+ });
25691
+ }
25692
+
25693
+ /**
25694
+ * The tiny observable-state kernel both flows are built on.
25695
+ *
25696
+ * Deliberately not a framework store: no React, no signals, no dependency.
25697
+ * A flow is a long asynchronous run that has to publish every transition to
25698
+ * whatever is rendering it, and has to be able to abandon a run that the
25699
+ * consumer walked away from. That is the whole requirement, and it is 60 lines.
25700
+ *
25701
+ * The `generation` counter is the abandonment mechanism. `reset()` bumps it;
25702
+ * an in-flight run captured the previous value, so every later `patch` it
25703
+ * attempts is dropped and every `isCurrent` checkpoint tells it to stop. It is
25704
+ * what makes `reset()` safe to call mid-flight — the run cannot resurrect a
25705
+ * dead state or, worse, drive the UI back into a phase the consumer has left.
25706
+ */
25707
+ class FlowStore {
25708
+ constructor(_initial) {
25709
+ this._initial = _initial;
25710
+ this._generation = 0;
25711
+ this._listeners = new Set();
25712
+ this._state = _initial;
25713
+ }
25714
+ get state() {
25715
+ return this._state;
25716
+ }
25717
+ /** The run token an in-flight pipeline captures at `start()`. */
25718
+ get generation() {
25719
+ return this._generation;
25720
+ }
25721
+ /** False once `reset()` (or a later `start()`) has superseded `token`. */
25722
+ isCurrent(token) {
25723
+ return this._generation === token;
25724
+ }
25725
+ /**
25726
+ * Subscribe to every transition. Returns the unsubscribe function; calling
25727
+ * it twice is harmless.
25728
+ *
25729
+ * Listeners are NOT called on subscribe — read `state` for the current
25730
+ * value. A listener that throws is not allowed to break the pipeline or
25731
+ * starve the listeners registered after it, so throws are swallowed.
25732
+ */
25733
+ subscribe(listener) {
25734
+ this._listeners.add(listener);
25735
+ return () => {
25736
+ this._listeners.delete(listener);
25737
+ };
25738
+ }
25739
+ /**
25740
+ * Merge `next` into the state and notify, unless `token` names a
25741
+ * superseded run.
25742
+ */
25743
+ patch(next, token) {
25744
+ if (token !== undefined && !this.isCurrent(token))
25745
+ return;
25746
+ this._state = Object.assign(Object.assign({}, this._state), next);
25747
+ for (const listener of [...this._listeners]) {
25748
+ try {
25749
+ listener(this._state);
25750
+ }
25751
+ catch (_a) {
25752
+ // A consumer's rendering fault is its own problem; the pipeline
25753
+ // owns real money and keeps going.
25754
+ }
25755
+ }
25756
+ }
25757
+ /**
25758
+ * Back to the initial state, abandoning any in-flight run.
25759
+ *
25760
+ * Subscribers are kept: the consumer that was watching the last run is the
25761
+ * one that will watch the next.
25762
+ */
25763
+ reset() {
25764
+ this._generation += 1;
25765
+ this.patch(this._initial);
25766
+ }
25767
+ /** Claim the next run token. Any earlier run is abandoned. */
25768
+ beginRun() {
25769
+ this._generation += 1;
25770
+ return this._generation;
25771
+ }
25772
+ }
25773
+
25774
+ /**
25775
+ * The run lifecycle both flows share: one run at a time, every transition
25776
+ * published, and a `reset()` that is safe to call in the middle of one.
25777
+ *
25778
+ * `DepositFlow` and `WithdrawFlow` each carried their own copy of this — the
25779
+ * re-entrancy guard, the `try/finally` around the run, `state`, `isRunning`,
25780
+ * `subscribe`, `reset`. Two copies of a guard is two places for a guard to be
25781
+ * wrong, on the money path, which is the same reason the pipelines themselves
25782
+ * moved out of the applications.
25783
+ *
25784
+ * ## The guard and the run token are separate, and both matter
25785
+ *
25786
+ * `_runToken` is the re-entrancy guard: claimed synchronously, before the
25787
+ * first `await`, so a double tap cannot launch two pipelines. The store's
25788
+ * `generation` is the abandonment mechanism: an in-flight run holds the value
25789
+ * it claimed, and every `patch` and `isCurrent` checkpoint it makes after
25790
+ * `reset()` is dropped.
25791
+ *
25792
+ * `reset()` releases the guard IMMEDIATELY rather than waiting for the
25793
+ * abandoned run to notice. It has to: a run parked on a wallet prompt that
25794
+ * never answers is a promise that may never settle, and a consumer that has
25795
+ * left that screen must be able to start a new run in the same tick. The
25796
+ * abandoned run's `finally` therefore releases the guard only if it still
25797
+ * holds it — by then a NEW run may own it, and clearing it there would let a
25798
+ * third run start alongside the second.
25799
+ */
25800
+ class Flow {
25801
+ constructor(initial) {
24576
25802
  /**
24577
25803
  * The token of the run currently holding the guard, or `null` when none
24578
25804
  * does. Not a boolean: releasing the guard has to be attributable to the
@@ -24776,219 +26002,32 @@ function buildLoanAgreementSignMessage(p) {
24776
26002
  * and the body sent must agree exactly, INCLUDING the address casing. This
24777
26003
  * builder lowercases, and the request body must carry the same lowercased
24778
26004
  * address; that is what both apps signing this format do today.
24779
- *
24780
- * The backend takes this path whenever the four human-readable display fields
24781
- * are absent, and documents it as permanent until the legacy app is
24782
- * decommissioned. `/contract/resolve` has no other format — every consumer
24783
- * signs this one to retrieve an existing agreement.
24784
- *
24785
- * @param timestampMs ms-epoch, and the same value sent as the body's
24786
- * `timestamp`.
24787
- */
24788
- function buildLegacyContractRequestMessage(address, timestampMs) {
24789
- return `I request contract content for ${address.toLowerCase()} at ${timestampMs}.`;
24790
- }
24791
- /**
24792
- * The `POST /contract/fullname` message.
24793
- *
24794
- * ⚠️ BYTE-EXACT PROTOCOL STRING, on the same terms as
24795
- * `buildLegacyContractRequestMessage`: kasu-backend rebuilds
24796
- * `` `I request my full name for ${address} at ${timestamp}.` `` from the
24797
- * request body and verifies the signature against it, so the body must carry
24798
- * the same lowercased address this builder signs.
24799
- *
24800
- * @param timestampMs ms-epoch, and the same value sent as the body's
24801
- * `timestamp`.
24802
- */
24803
- function buildFullNameRequestMessage(address, timestampMs) {
24804
- return `I request my full name for ${address.toLowerCase()} at ${timestampMs}.`;
24805
- }
24806
-
24807
- // ---------------------------------------------------------------------------
24808
- // The selector tables
24809
- // ---------------------------------------------------------------------------
24810
- /**
24811
- * OpenZeppelin v5's ERC-20 errors. They are not in either Kasu ABI because no
24812
- * Kasu contract declares them — they arrive from the TOKEN, through the
24813
- * manager's `transferFrom`, and they are the modern spelling of the string
24814
- * reverts below.
24815
- */
24816
- const ERC20_ERROR_ABI = [
24817
- 'error ERC20InsufficientBalance(address sender, uint256 balance, uint256 needed)',
24818
- 'error ERC20InsufficientAllowance(address spender, uint256 allowance, uint256 needed)',
24819
- ];
24820
- /**
24821
- * The protocol errors that mean the ERC-20 transfer failed rather than the
24822
- * protocol refusing.
24823
- *
24824
- * `SafeERC20FailedOperation` is OpenZeppelin's wrapper around a token call
24825
- * that returned false or reverted, which on this path IS the `transferFrom`;
24826
- * `AddressInsufficientBalance` is its native-value sibling. Both belong with
24827
- * the balance case, not with the pool conditions — a lender who sees one of
24828
- * them really does need to fund or re-approve.
24829
- */
24830
- const ERC20_FAMILY_NAMES = new Set([
24831
- 'AddressInsufficientBalance',
24832
- 'ERC20InsufficientAllowance',
24833
- 'ERC20InsufficientBalance',
24834
- 'SafeERC20FailedOperation',
24835
- ]);
24836
- /** Solidity's `Error(string)`, the selector every `require` reason arrives under. */
24837
- const STRING_REVERT_SELECTOR = '0x08c379a0';
24838
- /**
24839
- * The `require` reasons that are a balance or an allowance, across the token
24840
- * implementations in circulation: OpenZeppelin v4's sentences, and the
24841
- * shorthand the minimal implementations use.
24842
- */
24843
- const ERC20_REASON_PATTERNS = [
24844
- /exceeds\s+balance/,
24845
- /exceeds\s+allowance/,
24846
- /insufficient\s+balance/,
24847
- /insufficient\s+allowance/,
24848
- /transfer_from_failed/,
24849
- /transferfrom\s+failed/,
24850
- /low-level\s+call\s+failed/,
24851
- ];
24852
- /**
24853
- * selector → error name, built once on first use.
24854
- *
24855
- * Lazily, because a consumer that never sees a revert should not pay for three
24856
- * `Interface` constructions at import. The two Kasu tables are the GENERATED
24857
- * typechain ABIs, so this cannot drift from the contracts: regenerate the ABIs
24858
- * and the new errors are recognised with no edit here.
24859
- */
24860
- let selectorTable = null;
24861
- function selectors() {
24862
- if (selectorTable)
24863
- return selectorTable;
24864
- const table = new Map();
24865
- const interfaces = [
24866
- // The manager the deposit is called against, and the allow list it
24867
- // delegates the KYC and block-window checks to.
24868
- ILendingPoolManagerAbi__factory.createInterface(),
24869
- IKasuAllowListAbi__factory.createInterface(),
24870
- new ethers.utils.Interface(ERC20_ERROR_ABI),
24871
- ];
24872
- for (const iface of interfaces) {
24873
- for (const fragment of Object.values(iface.errors)) {
24874
- // First writer wins: the same name declared by both ABIs is the
24875
- // same selector, so the duplicate would be a no-op anyway.
24876
- const sighash = iface.getSighash(fragment);
24877
- if (!table.has(sighash))
24878
- table.set(sighash, fragment.name);
24879
- }
24880
- }
24881
- selectorTable = table;
24882
- return table;
24883
- }
24884
- // ---------------------------------------------------------------------------
24885
- // Getting the bytes out of the envelope
24886
- // ---------------------------------------------------------------------------
24887
- /**
24888
- * How deep to look for the revert data. Providers nest it differently —
24889
- * ethers' own error wraps the provider's, which wraps the RPC body — and a
24890
- * bound is what keeps a self-referencing error object from being walked
24891
- * forever.
24892
- */
24893
- const MAX_DEPTH = 6;
24894
- /** The keys ethers and the providers under it put a wrapped error on. */
24895
- const NESTED_KEYS = [
24896
- 'data',
24897
- 'error',
24898
- 'originalError',
24899
- 'info',
24900
- 'cause',
24901
- 'body',
24902
- ];
24903
- /** At least a 4-byte selector, and a whole number of bytes. */
24904
- function isRevertData(value) {
24905
- return (value.length >= 10 &&
24906
- value.length % 2 === 0 &&
24907
- /^0x[0-9a-fA-F]+$/.test(value));
24908
- }
24909
- /**
24910
- * Find the revert data on an error, wherever the provider left it.
24911
- *
24912
- * ethers v5 raises `UNPREDICTABLE_GAS_LIMIT` with the provider's own error
24913
- * attached, and each provider nests the bytes one layer differently:
24914
- * `error.data`, `error.error.data`, `error.data.originalError.data`, or only
24915
- * inside the JSON `body` of a `SERVER_ERROR`. Rather than enumerate the
24916
- * products, walk a fixed set of keys to a fixed depth and take the first value
24917
- * that is shaped like revert data.
24918
- */
24919
- function extractRevertData(err) {
24920
- return walk(err, 0);
24921
- }
24922
- function walk(node, depth) {
24923
- if (node == null || depth > MAX_DEPTH)
24924
- return null;
24925
- if (typeof node === 'string') {
24926
- if (isRevertData(node))
24927
- return node;
24928
- // A `SERVER_ERROR`'s body: the RPC's JSON reply, verbatim.
24929
- if (!node.startsWith('{'))
24930
- return null;
24931
- try {
24932
- return walk(JSON.parse(node), depth + 1);
24933
- }
24934
- catch (_a) {
24935
- return null;
24936
- }
24937
- }
24938
- if (typeof node !== 'object')
24939
- return null;
24940
- const record = node;
24941
- for (const key of NESTED_KEYS) {
24942
- const found = walk(record[key], depth + 1);
24943
- if (found)
24944
- return found;
24945
- }
24946
- return null;
26005
+ *
26006
+ * The backend takes this path whenever the four human-readable display fields
26007
+ * are absent, and documents it as permanent until the legacy app is
26008
+ * decommissioned. `/contract/resolve` has no other format — every consumer
26009
+ * signs this one to retrieve an existing agreement.
26010
+ *
26011
+ * @param timestampMs ms-epoch, and the same value sent as the body's
26012
+ * `timestamp`.
26013
+ */
26014
+ function buildLegacyContractRequestMessage(address, timestampMs) {
26015
+ return `I request contract content for ${address.toLowerCase()} at ${timestampMs}.`;
24947
26016
  }
24948
- // ---------------------------------------------------------------------------
24949
- // Decoding
24950
- // ---------------------------------------------------------------------------
24951
26017
  /**
24952
- * Decode a failed call's revert into the error the contract named, or `null`
24953
- * when there is nothing recognisable to name.
26018
+ * The `POST /contract/fullname` message.
24954
26019
  *
24955
- * `null` is the honest answer for an unknown selector, an unrelated `require`
24956
- * string, or an error with no revert data at all — the caller keeps whatever
24957
- * it did before rather than inventing a diagnosis from bytes it cannot read.
26020
+ * ⚠️ BYTE-EXACT PROTOCOL STRING, on the same terms as
26021
+ * `buildLegacyContractRequestMessage`: kasu-backend rebuilds
26022
+ * `` `I request my full name for ${address} at ${timestamp}.` `` from the
26023
+ * request body and verifies the signature against it, so the body must carry
26024
+ * the same lowercased address this builder signs.
26025
+ *
26026
+ * @param timestampMs ms-epoch, and the same value sent as the body's
26027
+ * `timestamp`.
24958
26028
  */
24959
- function decodeRevert(err) {
24960
- const data = extractRevertData(err);
24961
- if (!data)
24962
- return null;
24963
- const selector = data.slice(0, 10).toLowerCase();
24964
- if (selector === STRING_REVERT_SELECTOR) {
24965
- const reason = decodeStringRevert(data);
24966
- if (reason === null)
24967
- return null;
24968
- return isErc20Reason(reason)
24969
- ? { name: 'Error', family: 'erc20', reason }
24970
- : null;
24971
- }
24972
- const name = selectors().get(selector);
24973
- if (!name)
24974
- return null;
24975
- return {
24976
- name,
24977
- family: ERC20_FAMILY_NAMES.has(name) ? 'erc20' : 'protocol',
24978
- };
24979
- }
24980
- function decodeStringRevert(data) {
24981
- try {
24982
- const [reason] = ethers.utils.defaultAbiCoder.decode(['string'], `0x${data.slice(10)}`);
24983
- return reason;
24984
- }
24985
- catch (_a) {
24986
- return null;
24987
- }
24988
- }
24989
- function isErc20Reason(reason) {
24990
- const lower = reason.toLowerCase();
24991
- return ERC20_REASON_PATTERNS.some((pattern) => pattern.test(lower));
26029
+ function buildFullNameRequestMessage(address, timestampMs) {
26030
+ return `I request my full name for ${address.toLowerCase()} at ${timestampMs}.`;
24992
26031
  }
24993
26032
 
24994
26033
  /**
@@ -25146,31 +26185,6 @@ function classifyWalletFailure(step, err) {
25146
26185
  : { step, reason: 'failed', error: err };
25147
26186
  }
25148
26187
 
25149
- /**
25150
- * What a `depositBatch` port throws when it KNOWS the batch reverted — from a
25151
- * simulation before sending, or from the submitter refusing to broadcast a
25152
- * batch that would revert. The run ends `{ step: 'request', reason:
25153
- * 'reverted', revertError }`.
25154
- *
25155
- * `revertError` is the custom error the contract named (`'ClearingIsPending'`,
25156
- * `'UserNotKycd'`, …), exactly as the ABI declares it. Pass `''` when the
25157
- * batch is known to revert but not why: the consumer then renders its generic
25158
- * `reverted` copy, which is what an unknown name gets anyway.
25159
- *
25160
- * Any OTHER throw from the port is read the way a direct deposit's is — a
25161
- * wallet rejection is `cancelled`, a decodable revert is `reverted`, and
25162
- * everything else is `failed` with the error kept. A port that cannot tell is
25163
- * better off throwing what it has than guessing a name.
25164
- */
25165
- class BatchRevertedError extends Error {
25166
- constructor(revertError, message) {
25167
- super(message !== null && message !== void 0 ? message : `DepositFlow: the batch reverted${revertError ? ` with ${revertError}` : ''}`);
25168
- this.name = 'BatchRevertedError';
25169
- this.revertError = revertError;
25170
- // Keeps `instanceof` honest under a down-levelled build.
25171
- Object.setPrototypeOf(this, BatchRevertedError.prototype);
25172
- }
25173
- }
25174
26188
  /** Generated agreements are valid for five minutes upstream. */
25175
26189
  const CONTRACT_TTL_MS = 5 * 60 * 1000;
25176
26190
  /** What a run without a spender, from either source, fails with. */
@@ -25285,6 +26299,16 @@ function classifyRequest(err) {
25285
26299
  * deposit's throw is, so a non-wallet error can never become `cancelled`.
25286
26300
  */
25287
26301
  function classifyBatch(err) {
26302
+ // A submission outcome is read FIRST and taken at its word. It must never
26303
+ // reach the revert-data walk below: that walk follows `cause`, and an
26304
+ // ambiguous outcome whose cause happens to carry revert bytes would read
26305
+ // as `reverted` or `insufficient-balance` — "nothing was deposited, try
26306
+ // again" — on a batch that may have moved the money.
26307
+ if (err instanceof BatchSubmissionError) {
26308
+ return err.outcome === 'failed'
26309
+ ? { step: 'request', reason: 'failed', error: err }
26310
+ : { step: 'request', reason: err.outcome, error: err };
26311
+ }
25288
26312
  if (err instanceof BatchRevertedError) {
25289
26313
  return {
25290
26314
  step: 'request',
@@ -25810,16 +26834,50 @@ const INITIAL = {
25810
26834
  phase: 'idle',
25811
26835
  step: null,
25812
26836
  isMax: false,
26837
+ sponsored: false,
25813
26838
  failure: null,
25814
26839
  };
25815
26840
  /** What a KYC pre-check with no `buildKycParams` to call fails with. */
25816
26841
  const NO_KYC_PARAMS_MESSAGE = 'WithdrawFlow: getKycSignature was supplied without buildKycParams; build the flow with kasu.flows.withdraw() or pass both';
26842
+ /** What a sponsored run with no `encodeWithdrawal` to call fails with. */
26843
+ const NO_WITHDRAWAL_ENCODER_MESSAGE = 'WithdrawFlow: submitCall was supplied without encodeWithdrawal; build the flow with kasu.flows.withdraw() or pass both';
26844
+ /** What a sponsored run fails with when the encoder returned another kind of call. */
26845
+ const NOT_A_WITHDRAWAL_CALL_MESSAGE = 'WithdrawFlow: encodeWithdrawal returned a call that is not a withdrawal';
26846
+ /**
26847
+ * A `submitCall` / `wait()` rejection, read with the batch path's taxonomy. A
26848
+ * submission outcome is taken at its word FIRST — never through the wallet
26849
+ * classifier, which would read a relay's "request rejected" as the lender's
26850
+ * own doing. Then, from `submitCall` only, a wallet rejection is `cancelled`:
26851
+ * the lender declined to sign and nothing was posted — the same reading a raw
26852
+ * `depositBatch` throw gets. From `wait()` the call has already left, so a
26853
+ * rejection-worded error there is as ambiguous as any other. Everything
26854
+ * unrecognised is `pending`: it may have left.
26855
+ */
26856
+ function classifySponsored(err, from) {
26857
+ if (err instanceof BatchSubmissionError) {
26858
+ return err.outcome === 'failed'
26859
+ ? { step: 'request', reason: 'failed', error: err }
26860
+ : { step: 'request', reason: err.outcome, error: err };
26861
+ }
26862
+ if (err instanceof BatchRevertedError) {
26863
+ return {
26864
+ step: 'request',
26865
+ reason: 'reverted',
26866
+ revertError: err.revertError,
26867
+ error: err,
26868
+ };
26869
+ }
26870
+ if (from === 'submit' && isUserRejected(err)) {
26871
+ return { step: 'request', reason: 'cancelled' };
26872
+ }
26873
+ return { step: 'request', reason: 'pending', error: err };
26874
+ }
25817
26875
  // ---------------------------------------------------------------------------
25818
26876
  // The flow
25819
26877
  // ---------------------------------------------------------------------------
25820
26878
  class WithdrawFlow extends Flow {
25821
26879
  constructor(_ports) {
25822
- super(INITIAL);
26880
+ super(Object.assign(Object.assign({}, INITIAL), { sponsored: typeof _ports.submitCall === 'function' }));
25823
26881
  this._ports = _ports;
25824
26882
  }
25825
26883
  _run(input, token) {
@@ -25850,6 +26908,10 @@ class WithdrawFlow extends Flow {
25850
26908
  if (!this._store.isCurrent(token))
25851
26909
  return;
25852
26910
  }
26911
+ if (ports.submitCall) {
26912
+ yield this._runSponsored(input, token, isMax);
26913
+ return;
26914
+ }
25853
26915
  // 2. The submission. This one IS a wallet call, so it keeps the
25854
26916
  // `cancelled` / `failed` split.
25855
26917
  this._store.patch({ phase: 'request-sign', step: 'request', isMax }, token);
@@ -25876,6 +26938,72 @@ class WithdrawFlow extends Flow {
25876
26938
  this._store.patch({ phase: 'success' }, token);
25877
26939
  });
25878
26940
  }
26941
+ /**
26942
+ * 2'. The sponsored submission: encode, re-check the run, submit, settle.
26943
+ *
26944
+ * The encode reads the chain (the shares), so a run abandoned during it
26945
+ * must stop BEFORE `submitCall` — nothing has been sent yet, and an
26946
+ * abandoned run must never send. A throw while encoding is `failed`:
26947
+ * nothing left. After `submitCall` is called, only the taxonomy decides.
26948
+ */
26949
+ _runSponsored(input, token, isMax) {
26950
+ return __awaiter(this, void 0, void 0, function* () {
26951
+ const ports = this._ports;
26952
+ // Only reached when `submitCall` is supplied; narrows it for below.
26953
+ if (!ports.submitCall)
26954
+ return;
26955
+ this._store.patch({ phase: 'request-sign', step: 'request', isMax }, token);
26956
+ let call;
26957
+ try {
26958
+ if (!ports.encodeWithdrawal) {
26959
+ throw new Error(NO_WITHDRAWAL_ENCODER_MESSAGE);
26960
+ }
26961
+ call = yield ports.encodeWithdrawal({
26962
+ poolId: input.poolId,
26963
+ trancheId: input.trancheId,
26964
+ amount: input.amount,
26965
+ userAddress: input.userAddress,
26966
+ });
26967
+ if ((call === null || call === void 0 ? void 0 : call.kind) !== 'withdrawal') {
26968
+ throw new Error(NOT_A_WITHDRAWAL_CALL_MESSAGE);
26969
+ }
26970
+ }
26971
+ catch (err) {
26972
+ this._store.patch({
26973
+ phase: 'error',
26974
+ failure: { step: 'request', reason: 'failed', error: err },
26975
+ }, token);
26976
+ return;
26977
+ }
26978
+ if (!this._store.isCurrent(token))
26979
+ return;
26980
+ let tx;
26981
+ try {
26982
+ tx = yield ports.submitCall(call);
26983
+ }
26984
+ catch (err) {
26985
+ this._store.patch({ phase: 'error', failure: classifySponsored(err, 'submit') }, token);
26986
+ return;
26987
+ }
26988
+ // The call has left. A run abandoned meanwhile stops HERE, without
26989
+ // `wait()`: nothing it could learn would be published, and a sponsored
26990
+ // `wait()` is minutes of polling against the submitter, not a cheap
26991
+ // receipt read. The submission itself cannot be taken back.
26992
+ if (!this._store.isCurrent(token))
26993
+ return;
26994
+ this._store.patch({ phase: 'request-confirm' }, token);
26995
+ try {
26996
+ yield tx.wait();
26997
+ }
26998
+ catch (err) {
26999
+ this._store.patch({ phase: 'error', failure: classifySponsored(err, 'wait') }, token);
27000
+ return;
27001
+ }
27002
+ if (!this._store.isCurrent(token))
27003
+ return;
27004
+ this._store.patch({ phase: 'success' }, token);
27005
+ });
27006
+ }
25879
27007
  }
25880
27008
 
25881
27009
  /**
@@ -25922,12 +27050,20 @@ class FlowsFacade {
25922
27050
  * approve. `undefined` for a hand-built config without `stableAsset`,
25923
27051
  * in which case a run never batches: it does not guess a token.
25924
27052
  */
25925
- _stableAsset) {
27053
+ _stableAsset,
27054
+ /**
27055
+ * This instance's call encoders — the sponsored withdraw path's
27056
+ * default `encodeWithdrawal`. Optional so a hand-built facade keeps
27057
+ * constructing; without it a sponsored run fails (`failed`, nothing
27058
+ * sent) unless the consumer passes `encodeWithdrawal` itself.
27059
+ */
27060
+ _calls) {
25926
27061
  this._deposits = _deposits;
25927
27062
  this._erc20 = _erc20;
25928
27063
  this._isReadOnly = _isReadOnly;
25929
27064
  this._spender = _spender;
25930
27065
  this._stableAsset = _stableAsset;
27066
+ this._calls = _calls;
25931
27067
  }
25932
27068
  /**
25933
27069
  * A deposit pipeline. `readAllowance`, `approve`, `deposit` and
@@ -25974,10 +27110,13 @@ class FlowsFacade {
25974
27110
  /**
25975
27111
  * A withdrawal pipeline. Both write ports and `buildKycParams` default to
25976
27112
  * this instance; supplying `getKycSignature` turns the KYC pre-check on.
27113
+ * Supplying `submitCall` turns the sponsored path on, and its encoder
27114
+ * defaults to `kasu.calls.requestWithdrawal` on this instance — which
27115
+ * reads, never writes, so it works on a read-only instance too.
25977
27116
  * Defaults are applied per key, for the reason `deposit()` gives.
25978
27117
  */
25979
27118
  withdraw(ports = {}) {
25980
- var _a, _b, _c;
27119
+ var _a, _b, _c, _d;
25981
27120
  return new WithdrawFlow({
25982
27121
  buildKycParams: (_a = ports.buildKycParams) !== null && _a !== void 0 ? _a : ((userAddress) => this._deposits.buildKycParams(userAddress)),
25983
27122
  getKycSignature: ports.getKycSignature,
@@ -25987,8 +27126,17 @@ class FlowsFacade {
25987
27126
  amount: params.amount,
25988
27127
  })),
25989
27128
  withdrawMax: (_c = ports.withdrawMax) !== null && _c !== void 0 ? _c : ((poolId, trancheId, userAddress) => this._deposits.withdrawMax(poolId, trancheId, userAddress)),
27129
+ submitCall: ports.submitCall,
27130
+ encodeWithdrawal: (_d = ports.encodeWithdrawal) !== null && _d !== void 0 ? _d : this._defaultEncoder(),
25990
27131
  });
25991
27132
  }
27133
+ /** `kasu.calls.requestWithdrawal`, when this facade was given the encoders. */
27134
+ _defaultEncoder() {
27135
+ const calls = this._calls;
27136
+ if (!calls)
27137
+ return undefined;
27138
+ return (params) => calls.requestWithdrawal(params);
27139
+ }
25992
27140
  _assertWritable() {
25993
27141
  if (this._isReadOnly) {
25994
27142
  throw new Error(READ_ONLY_MESSAGE);
@@ -26283,9 +27431,72 @@ function maxNetRateCeiling(pools, feePercent) {
26283
27431
  * Handles epoch fetching internally so callers never need to know about epochs.
26284
27432
  */
26285
27433
  class StrategiesFacade {
26286
- constructor(_dataService, _userLending) {
27434
+ constructor(_dataService, _userLending,
27435
+ /**
27436
+ * Chain pool id → the pool id its CMS record is keyed by (XDC pools
27437
+ * share their Base counterpart's editorial record). Lowercase keys.
27438
+ */
27439
+ _poolMetadataMapping = {}) {
26287
27440
  this._dataService = _dataService;
26288
27441
  this._userLending = _userLending;
27442
+ this._poolMetadataMapping = _poolMetadataMapping;
27443
+ }
27444
+ /**
27445
+ * The pool's loss rate and first-loss capital, from its CMS
27446
+ * risk-management record. `null` when the pool has no record.
27447
+ *
27448
+ * Each figure is a NUMBER or `null` — never a guess. The CMS returns
27449
+ * "numeric" fields as a number, a numeric string, an empty string or
27450
+ * null, and an empty string must not become a confident `0`: anything that
27451
+ * is not a finite number reads as `null` (render "—"). `poolLossRate` is a
27452
+ * 0..1 fraction.
27453
+ *
27454
+ * Requires `directusUrl`; without it the call rejects with
27455
+ * `NO_DIRECTUS_URL_MESSAGE`, like every CMS-only read.
27456
+ */
27457
+ getRiskPerformance(poolId) {
27458
+ return __awaiter(this, void 0, void 0, function* () {
27459
+ const metadataId = this._metadataIdOf(poolId);
27460
+ const records = yield this._dataService.getRiskManagement();
27461
+ const record = records.find((r) => String(r.poolIdFK).toLowerCase() === metadataId);
27462
+ if (!record)
27463
+ return null;
27464
+ const performance = record.riskPerformance;
27465
+ return {
27466
+ poolLossRate: finiteOrNull(performance === null || performance === void 0 ? void 0 : performance.poolLossRate),
27467
+ firstLossCapital: finiteOrNull(performance === null || performance === void 0 ? void 0 : performance.firstLossCapital),
27468
+ };
27469
+ });
27470
+ }
27471
+ /**
27472
+ * The credit originator's profile and lending history for a pool — the
27473
+ * CMS record whose `otherKASUPools` lists it. `null` when none does.
27474
+ *
27475
+ * Matched on the chain pool id, then on its metadata id, case-
27476
+ * insensitively. The record is returned as the service maps it; its
27477
+ * numeric fields are CMS-authored, so read them defensively.
27478
+ *
27479
+ * The service filters `otherKASUPools` through the CURRENT chain's
27480
+ * subgraph with the CMS record's (Base-keyed) ids, so on an XDC
27481
+ * deployment every profile lists no pools and this resolves `null` —
27482
+ * the same answer kasu-ui's own lookup gives there today. The metadata
27483
+ * candidate becomes live once the service resolves the mapping.
27484
+ */
27485
+ getOriginator(poolId) {
27486
+ return __awaiter(this, void 0, void 0, function* () {
27487
+ var _a;
27488
+ const candidates = new Set([
27489
+ poolId.toLowerCase(),
27490
+ this._metadataIdOf(poolId),
27491
+ ]);
27492
+ const profiles = yield this._dataService.getPoolDelegateProfileAndHistory();
27493
+ return ((_a = profiles.find((profile) => profile.otherKASUPools.some((pool) => candidates.has(String(pool.id).toLowerCase())))) !== null && _a !== void 0 ? _a : null);
27494
+ });
27495
+ }
27496
+ _metadataIdOf(poolId) {
27497
+ var _a;
27498
+ const lower = poolId.toLowerCase();
27499
+ return ((_a = this._poolMetadataMapping[lower]) !== null && _a !== void 0 ? _a : lower).toLowerCase();
26289
27500
  }
26290
27501
  /**
26291
27502
  * Fetch all active lending strategies.
@@ -26364,9 +27575,7 @@ class StrategiesFacade {
26364
27575
  */
26365
27576
  calculateDepositLimits(tranche) {
26366
27577
  const raw = tranche._raw;
26367
- const min = parseFloat(raw.minimumDeposit) < 1
26368
- ? '1'
26369
- : raw.minimumDeposit;
27578
+ const min = parseFloat(raw.minimumDeposit) < 1 ? '1' : raw.minimumDeposit;
26370
27579
  const max = parseFloat(raw.maximumDeposit) > parseFloat(raw.poolCapacity)
26371
27580
  ? raw.poolCapacity
26372
27581
  : raw.maximumDeposit;
@@ -26414,14 +27623,146 @@ class StrategiesFacade {
26414
27623
  };
26415
27624
  }
26416
27625
  }
27626
+ /**
27627
+ * A CMS "numeric" field as a finite number, or `null`. An empty or blank
27628
+ * string is UNAUTHORED, not zero.
27629
+ */
27630
+ function finiteOrNull(value) {
27631
+ if (typeof value === 'number')
27632
+ return Number.isFinite(value) ? value : null;
27633
+ if (typeof value === 'string') {
27634
+ if (value.trim() === '')
27635
+ return null;
27636
+ const numeric = Number(value);
27637
+ return Number.isFinite(numeric) ? numeric : null;
27638
+ }
27639
+ return null;
27640
+ }
27641
+
27642
+ var UserRequestStatus;
27643
+ (function (UserRequestStatus) {
27644
+ UserRequestStatus["REQUESTED"] = "Requested";
27645
+ UserRequestStatus["PROCESSING"] = "Processing";
27646
+ UserRequestStatus["PROCESSED"] = "Processed";
27647
+ })(UserRequestStatus || (UserRequestStatus = {}));
27648
+
27649
+ /**
27650
+ * Cycles and the clearing window — the protocol's weekly clock, as numbers.
27651
+ *
27652
+ * Lifted verbatim from kasu-ui's `features/portfolio/lib/settlement-window.ts`
27653
+ * and `features/lending/lib/cycle-dates.ts`. The two formatters that live
27654
+ * beside `deriveCycleDates` there (`formatCycleDate`, `formatCycleCloseUtc`)
27655
+ * print words and stay in the applications; everything here is unix seconds in
27656
+ * and unix seconds out.
27657
+ *
27658
+ * The clearing window is the fixed 48 hours immediately preceding an epoch
27659
+ * end. Inside it, pending requests are being processed and cannot be modified,
27660
+ * and the countdown runs to the epoch end. Outside it, the countdown runs to
27661
+ * the next clearing-window start.
27662
+ *
27663
+ * The epoch end comes straight from the chain (`nextEpochStartTimestamp`, i.e.
27664
+ * the SDK's `getNextEpochDate`) — the same value the protocol's own
27665
+ * `getNextClearingPeriodDate` derives from — so the window always lines up
27666
+ * with the real weekly schedule (Tue 06:00 → Thu 06:00 UTC on Base) instead of
27667
+ * a projected subgraph timestamp that can drift off the grid.
27668
+ */
27669
+ /**
27670
+ * Clearing-window length — a fixed 48h protocol constant. Exported so every
27671
+ * consumer derives the window from the same number this module runs on.
27672
+ */
27673
+ const CLEARING_WINDOW_SECONDS = 48 * 60 * 60;
27674
+ /**
27675
+ * Weekly cadence — the epoch schedule is fixed weekly (Tue → Thu UTC on Base).
27676
+ * Used only to roll a cycle forward when a request lands inside a window that
27677
+ * has already closed.
27678
+ */
27679
+ const WEEK_SECONDS = 7 * 24 * 60 * 60;
27680
+ /**
27681
+ * Which phase of the weekly cycle `nowSeconds` falls in, and how long is left
27682
+ * of it.
27683
+ *
27684
+ * `'unknown'` when no epoch boundary has been loaded yet, or when the one on
27685
+ * hand is stale (it has already elapsed — the on-chain value refetches to the
27686
+ * next boundary shortly after rollover). A caller must render its "no cycle
27687
+ * loaded" state there, never a zeroed countdown.
27688
+ */
27689
+ function computeSettlementWindow({ nowSeconds, nextEpochStart, clearingWindowSeconds = CLEARING_WINDOW_SECONDS, }) {
27690
+ // No epoch boundary loaded yet, or a stale one that already elapsed.
27691
+ if (nextEpochStart <= 0 || nextEpochStart <= nowSeconds) {
27692
+ return { phase: 'unknown' };
27693
+ }
27694
+ const clearingStart = nextEpochStart - clearingWindowSeconds;
27695
+ if (nowSeconds < clearingStart) {
27696
+ return {
27697
+ phase: 'awaiting',
27698
+ secondsUntilClearing: clearingStart - nowSeconds,
27699
+ nextClearingStart: clearingStart,
27700
+ };
27701
+ }
27702
+ // nowSeconds is in [clearingStart, nextEpochStart) — inside the window.
27703
+ return {
27704
+ phase: 'clearing',
27705
+ secondsUntilEpochEnd: nextEpochStart - nowSeconds,
27706
+ epochEnd: nextEpochStart,
27707
+ };
27708
+ }
27709
+ /**
27710
+ * The next cycle boundary strictly after `nowSeconds`, in unix seconds — the
27711
+ * cycle close (`nextEpochStart − 48h`) while the window is still open, the
27712
+ * epoch end once we are inside it.
27713
+ *
27714
+ * `undefined` when there is no boundary left to wait for: no epoch boundary
27715
+ * loaded, or a cached one that has already elapsed — the same staleness rule
27716
+ * `computeSettlementWindow` applies before it reports `'unknown'`.
27717
+ *
27718
+ * Split out of the state machine because some consumers need the INSTANT
27719
+ * rather than the phase: one to flush the cycle-dependent caches when the
27720
+ * clock crosses it, one to move a pre-commit screen's snapshot clock at the
27721
+ * same moment.
27722
+ */
27723
+ function nextCycleBoundary(nextEpochStart, nowSeconds, clearingWindowSeconds = CLEARING_WINDOW_SECONDS) {
27724
+ if (!nextEpochStart || nextEpochStart <= 0)
27725
+ return undefined;
27726
+ const clearingStart = nextEpochStart - clearingWindowSeconds;
27727
+ if (nowSeconds < clearingStart)
27728
+ return clearingStart;
27729
+ if (nowSeconds < nextEpochStart)
27730
+ return nextEpochStart;
27731
+ return undefined;
27732
+ }
27733
+ /**
27734
+ * The cycle-close and outcome dates for a request submitted `now`.
27735
+ *
27736
+ * - The cycle "closes" (stops accepting requests, starts processing) at the
27737
+ * start of the 48h clearing window, i.e. 48h before the epoch end.
27738
+ * - Processing takes up to 48h, so the outcome is confirmed by the epoch end
27739
+ * (close + 48h).
27740
+ *
27741
+ * Returns `null` when the epoch boundary is not available or is stale — a
27742
+ * caller then omits the dates entirely (omit, don't stub).
27743
+ *
27744
+ * The common case is a request submitted OUTSIDE the clearing window: the
27745
+ * close is `nextEpochStart − 48h` and the outcome is `nextEpochStart`. When
27746
+ * the request lands INSIDE the current clearing window (that close is already
27747
+ * in the past), it queues for the NEXT weekly cycle, so the close is advanced
27748
+ * by whole weeks until it is in the future.
27749
+ */
27750
+ function deriveCycleDates(nextEpochStart, nowSeconds) {
27751
+ // No boundary loaded, or a stale one that already elapsed — the same
27752
+ // staleness rule as `computeSettlementWindow`.
27753
+ if (!nextEpochStart || nextEpochStart <= nowSeconds)
27754
+ return null;
27755
+ let close = nextEpochStart - CLEARING_WINDOW_SECONDS;
27756
+ let outcome = nextEpochStart;
27757
+ // Inside the current clearing window the close already passed; a request
27758
+ // now is queued for the next weekly cycle.
27759
+ while (close <= nowSeconds) {
27760
+ close += WEEK_SECONDS;
27761
+ outcome += WEEK_SECONDS;
27762
+ }
27763
+ return { close, outcome };
27764
+ }
26417
27765
 
26418
- var UserRequestStatus;
26419
- (function (UserRequestStatus) {
26420
- UserRequestStatus["REQUESTED"] = "Requested";
26421
- UserRequestStatus["PROCESSING"] = "Processing";
26422
- UserRequestStatus["PROCESSED"] = "Processed";
26423
- })(UserRequestStatus || (UserRequestStatus = {}));
26424
-
26425
27766
  /**
26426
27767
  * Events that represent a lender SUBMISSION into the bundle. A dNFT position
26427
27768
  * aggregates every submission the lender made into the same pool/tranche this
@@ -26482,6 +27823,38 @@ function lastEventTimestamp(events, fallback) {
26482
27823
  function isCycleClosed(request) {
26483
27824
  return !request.canCancel;
26484
27825
  }
27826
+ /**
27827
+ * Until when can THIS request still be cancelled? — unix seconds, or `null`
27828
+ * when it cannot be cancelled at all, or when no epoch boundary is loaded.
27829
+ *
27830
+ * The answer is the next cycle close (`deriveCycleDates(...).close`): the
27831
+ * clearing window opens there, and the contract refuses a cancel while the
27832
+ * pool's clearing is pending (`CannotCancelRequestIfClearingIsPending`).
27833
+ * Inside a window whose clearing already ENDED, a still-cancellable request is
27834
+ * queued for the next cycle, so its deadline is next week's close — which is
27835
+ * exactly what `deriveCycleDates` rolls forward to.
27836
+ *
27837
+ * `null` unless the request is BOTH:
27838
+ *
27839
+ * - `canCancel` — the SDK's per-pool signal (see `isCycleClosed`);
27840
+ * - live: `statusCode` `pending`, or `partial` (a partly-filled withdrawal
27841
+ * whose remainder is back in the queue — a resolved partial has
27842
+ * `canCancel: false`). A cancelled, forced, reallocated, rejected or complete
27843
+ * request has nothing left to cancel, whatever `canCancel` says.
27844
+ *
27845
+ * `canCancel` is read ONCE, when the request was fetched. A request held
27846
+ * across a cycle boundary (`nextCycleBoundary`) must be refetched before this
27847
+ * is shown again — the deadline is a hint, the contract is the gate.
27848
+ */
27849
+ function cancelDeadline(request, nextEpochStart, nowSeconds) {
27850
+ var _a, _b;
27851
+ if (!request.canCancel)
27852
+ return null;
27853
+ if (request.statusCode !== 'pending' && request.statusCode !== 'partial') {
27854
+ return null;
27855
+ }
27856
+ return (_b = (_a = deriveCycleDates(nextEpochStart, nowSeconds)) === null || _a === void 0 ? void 0 : _a.close) !== null && _b !== void 0 ? _b : null;
27857
+ }
26485
27858
  const isCancelled = (events) => events.some((e) => e.requestType === 'Cancelled');
26486
27859
  const isForced = (events) => events.some((e) => e.requestType === 'Forced');
26487
27860
  const initiatedEvent = (events) => events.find((e) => e.requestType === 'Initiated');
@@ -26708,8 +28081,8 @@ class PortfolioFacade {
26708
28081
  /**
26709
28082
  * High-level entry point for external integrators.
26710
28083
  *
26711
- * Provides four domain facades — `strategies`, `deposits`, `portfolio`,
26712
- * `flows` — and exposes the underlying `KasuSdk` services via `.services` for
28084
+ * Provides five domain facades — `strategies`, `deposits`, `portfolio`,
28085
+ * `flows`, `calls` — and exposes the underlying `KasuSdk` services via `.services` for
26713
28086
  * power-users.
26714
28087
  *
26715
28088
  * ```ts
@@ -26729,19 +28102,22 @@ class PortfolioFacade {
26729
28102
  */
26730
28103
  class Kasu {
26731
28104
  constructor(sdk, chainConfig, signerOrProvider, configOverrides) {
26732
- var _a;
28105
+ var _a, _b;
26733
28106
  this._sdk = sdk;
26734
28107
  this._chainConfig = chainConfig;
26735
28108
  this._signerOrProvider = signerOrProvider;
26736
28109
  this._configOverrides = configOverrides;
26737
- this.strategies = new StrategiesFacade(sdk.DataService, sdk.UserLending);
28110
+ this.strategies = new StrategiesFacade(sdk.DataService, sdk.UserLending, normaliseMapping((_a = configOverrides.poolMetadataMapping) !== null && _a !== void 0 ? _a : chainConfig.poolMetadataMapping));
26738
28111
  // Derived ONCE and handed to every facade that needs it. Each of them
26739
28112
  // re-deriving `Signer.isSigner` is how two facades come to disagree
26740
28113
  // about whether the same instance can write.
26741
28114
  const isReadOnly = !Signer.isSigner(signerOrProvider);
26742
28115
  this.deposits = new DepositsFacade(sdk.UserLending, chainConfig.chainId.toString(), isReadOnly);
26743
28116
  this.portfolio = new PortfolioFacade(sdk.DataService, sdk.UserLending, sdk.Portfolio);
26744
- this.flows = new FlowsFacade(this.deposits, () => erc20Of(chainConfig, signerOrProvider), isReadOnly, chainConfig.contracts.LendingPoolManager, (_a = stableAssetOf(chainConfig)) === null || _a === void 0 ? void 0 : _a.address);
28117
+ // The same `LendingPoolManager` the deposit flow approves and calls:
28118
+ // the chain config's, which is what a relay allow-lists `to` against.
28119
+ this.calls = new CallsFacade(sdk.UserLending, chainConfig.contracts.LendingPoolManager);
28120
+ this.flows = new FlowsFacade(this.deposits, () => erc20Of(chainConfig, signerOrProvider), isReadOnly, chainConfig.contracts.LendingPoolManager, (_b = stableAssetOf(chainConfig)) === null || _b === void 0 ? void 0 : _b.address, this.calls);
26745
28121
  }
26746
28122
  /**
26747
28123
  * Create a Kasu instance with built-in chain config.
@@ -26926,6 +28302,14 @@ function resolveChainConfig(chain) {
26926
28302
  }
26927
28303
  return chain;
26928
28304
  }
28305
+ /** Lowercase both sides, so a lookup by any casing of a pool id matches. */
28306
+ function normaliseMapping(mapping) {
28307
+ const out = {};
28308
+ for (const [from, to] of Object.entries(mapping !== null && mapping !== void 0 ? mapping : {})) {
28309
+ out[from.toLowerCase()] = to.toLowerCase();
28310
+ }
28311
+ return out;
28312
+ }
26929
28313
 
26930
28314
  /** Directus instance holding Kasu's pool editorial content. */
26931
28315
  const DEFAULT_DIRECTUS_URL = 'https://kasu-finance.directus.app/';
@@ -27213,17 +28597,143 @@ function ceilToCents(n) {
27213
28597
  return snapped < n ? (k + 1) / CENTS : snapped;
27214
28598
  }
27215
28599
 
28600
+ const TX_HASH = /^0x[0-9a-fA-F]{64}$/;
28601
+ const ADDRESS = /^0x[0-9a-fA-F]{40}$/;
28602
+ /** `https://host[:port][/path]`, no query, fragment, credentials or space. */
28603
+ const HTTPS_BASE = /^https:\/\/[A-Za-z0-9.-]+(:[0-9]{1,5})?(\/[A-Za-z0-9._~\-/]*)?$/;
28604
+ function explorerBase(config) {
28605
+ // Read defensively: a custom config written before 2.10.0 has no field,
28606
+ // and TypeScript only tells its author once they bump.
28607
+ const base = config === null || config === void 0 ? void 0 : config.blockExplorerUrl;
28608
+ if (typeof base !== 'string' || !HTTPS_BASE.test(base)) {
28609
+ throw new Error('explorer url: the chain config has no valid https blockExplorerUrl');
28610
+ }
28611
+ return base.replace(/\/+$/, '');
28612
+ }
28613
+ /**
28614
+ * `<blockExplorerUrl>/tx/<txHash>`. Throws unless `txHash` is exactly `0x`
28615
+ * followed by 64 hex digits.
28616
+ */
28617
+ function explorerTxUrl(config, txHash) {
28618
+ if (typeof txHash !== 'string' || !TX_HASH.test(txHash)) {
28619
+ throw new Error('explorerTxUrl: not a 32-byte 0x-prefixed hex hash');
28620
+ }
28621
+ return `${explorerBase(config)}/tx/${txHash}`;
28622
+ }
28623
+ /**
28624
+ * `<blockExplorerUrl>/address/<address>`, the address as given. Throws unless
28625
+ * `address` is `0x` followed by 40 hex digits with a valid checksum when it
28626
+ * is mixed-case (all-lower and all-upper carry no checksum and are accepted).
28627
+ */
28628
+ function explorerAddressUrl(config, address) {
28629
+ if (typeof address !== 'string' ||
28630
+ !ADDRESS.test(address) ||
28631
+ !utils$2.isAddress(address)) {
28632
+ throw new Error('explorerAddressUrl: not a valid 0x-prefixed address');
28633
+ }
28634
+ return `${explorerBase(config)}/address/${address}`;
28635
+ }
28636
+
28637
+ /**
28638
+ * One epoch's nominal length in days. A lock is counted in epochs, and the
28639
+ * loan-agreement text states its term from the same 7-day epoch.
28640
+ */
28641
+ const DAYS_PER_EPOCH = 7;
28642
+ /**
28643
+ * The average calendar month, 365.25 / 12. A lock is counted in epochs, so a
28644
+ * "12-month" term is 52 epochs = 364 days, and only the average-month divisor
28645
+ * lands that on 12 (364 / 30.4375 = 11.96 → 12).
28646
+ */
28647
+ const DAYS_PER_MONTH = 30.4375;
28648
+ /**
28649
+ * The shortest lock stated in months. Below it a term reads in weeks or days,
28650
+ * so nothing may claim a month count for it — a 3-epoch lock is 21 days, which
28651
+ * the average-month divisor would otherwise round UP to one month.
28652
+ */
28653
+ const MIN_DAYS_FOR_MONTHS = 30;
28654
+ /**
28655
+ * `'0'`, empty or absent is the variable-rate sentinel, not a fixed term.
28656
+ * Every surface that tags a rate "variable" or "fixed" must derive the tag
28657
+ * through this, so no screen can hardcode a tag that contradicts the rate
28658
+ * beside it.
28659
+ */
28660
+ function isFixedTermConfigId(configId) {
28661
+ if (configId == null)
28662
+ return false;
28663
+ const trimmed = configId.trim();
28664
+ return trimmed !== '' && trimmed !== '0';
28665
+ }
28666
+ /**
28667
+ * The fixed-term configs this wallet may actually select on a tranche: a
28668
+ * config open to `Everyone`, or an `AllowlistedOnly` config whose allowlist
28669
+ * names the wallet. Anything else is dropped (hide-ineligible): the contract
28670
+ * refuses a deposit into an allowlisted config from a wallet that is not on
28671
+ * it, so offering one is offering a transaction that reverts.
28672
+ *
28673
+ * Address matching is case-insensitive. An allowlist entry whose
28674
+ * `isAllowlisted` is `false` is a wallet that was REMOVED — the subgraph keeps
28675
+ * the entity and flips the flag — so it does not count. An entry that carries
28676
+ * no flag at all counts by its presence, which is how every entry read before
28677
+ * 2.9.0 arrived (the query did not select the field).
28678
+ *
28679
+ * `address` `null` / `undefined` — no wallet yet — sees only the `Everyone`
28680
+ * configs. Order is the tranche's own.
28681
+ */
28682
+ function availableFixedTermConfigs(tranche, address) {
28683
+ if (!tranche)
28684
+ return [];
28685
+ const lower = address === null || address === void 0 ? void 0 : address.toLowerCase();
28686
+ return tranche.fixedTermConfig.filter((config) => config.fixedTermDepositStatus === 'Everyone' ||
28687
+ (lower != null &&
28688
+ lower !== '' &&
28689
+ config.fixedTermDepositAllowlist.some((entry) => entry.isAllowlisted !==
28690
+ false && entry.userId.toLowerCase() === lower)));
28691
+ }
28692
+ /**
28693
+ * A lock's length in days: `epochLockDuration × 7`.
28694
+ *
28695
+ * FAILS CLOSED: an unparseable, non-finite, zero or negative epoch count
28696
+ * returns `NaN`, never `0` — a zero-day lock is not a term, and a consumer
28697
+ * that rendered it would state one.
28698
+ *
28699
+ * @param epochLockDuration the config's `epochLockDuration`, a decimal string.
28700
+ */
28701
+ function lockDurationDays(epochLockDuration) {
28702
+ const epochs = parseFloat(epochLockDuration);
28703
+ if (!Number.isFinite(epochs) || epochs <= 0)
28704
+ return NaN;
28705
+ return epochs * DAYS_PER_EPOCH;
28706
+ }
28707
+ /**
28708
+ * A lock in WHOLE months, or `null` when the epoch count is not a positive
28709
+ * number or the lock is shorter than `MIN_DAYS_FOR_MONTHS`.
28710
+ *
28711
+ * Unlike a year-rounding formatter it never rolls up into years: a 104-epoch
28712
+ * lock is 24 months. A consumer that states years does that conversion in its
28713
+ * own words.
28714
+ */
28715
+ function lockDurationMonths(epochLockDuration) {
28716
+ const days = lockDurationDays(epochLockDuration);
28717
+ if (Number.isNaN(days) || days < MIN_DAYS_FOR_MONTHS)
28718
+ return null;
28719
+ const months = Math.round(days / DAYS_PER_MONTH);
28720
+ return months > 0 ? months : null;
28721
+ }
28722
+
27216
28723
  const APXIUM = {
27217
28724
  name: 'Apxium',
27218
28725
  url: 'https://www.apxium.com/',
28726
+ region: 'Australia',
27219
28727
  };
27220
28728
  const INVOICEMATE = {
27221
28729
  name: 'InvoiceMate',
27222
28730
  url: 'https://invoicemate.net/',
28731
+ region: 'Global',
27223
28732
  };
27224
28733
  const RIXON_CAPITAL = {
27225
28734
  name: 'Rixon Capital',
27226
28735
  url: 'https://rixon.capital/',
28736
+ region: 'Australia',
27227
28737
  };
27228
28738
  function poolNameOf(pool) {
27229
28739
  var _a, _b;
@@ -27255,122 +28765,22 @@ function getCreditOriginator(pool) {
27255
28765
  function getInstitutionalLender(originator) {
27256
28766
  return originator.name === APXIUM.name ? RIXON_CAPITAL : null;
27257
28767
  }
27258
-
27259
- /**
27260
- * Cycles and the clearing window — the protocol's weekly clock, as numbers.
27261
- *
27262
- * Lifted verbatim from kasu-ui's `features/portfolio/lib/settlement-window.ts`
27263
- * and `features/lending/lib/cycle-dates.ts`. The two formatters that live
27264
- * beside `deriveCycleDates` there (`formatCycleDate`, `formatCycleCloseUtc`)
27265
- * print words and stay in the applications; everything here is unix seconds in
27266
- * and unix seconds out.
27267
- *
27268
- * The clearing window is the fixed 48 hours immediately preceding an epoch
27269
- * end. Inside it, pending requests are being processed and cannot be modified,
27270
- * and the countdown runs to the epoch end. Outside it, the countdown runs to
27271
- * the next clearing-window start.
27272
- *
27273
- * The epoch end comes straight from the chain (`nextEpochStartTimestamp`, i.e.
27274
- * the SDK's `getNextEpochDate`) — the same value the protocol's own
27275
- * `getNextClearingPeriodDate` derives from — so the window always lines up
27276
- * with the real weekly schedule (Tue 06:00 → Thu 06:00 UTC on Base) instead of
27277
- * a projected subgraph timestamp that can drift off the grid.
27278
- */
27279
- /**
27280
- * Clearing-window length — a fixed 48h protocol constant. Exported so every
27281
- * consumer derives the window from the same number this module runs on.
27282
- */
27283
- const CLEARING_WINDOW_SECONDS = 48 * 60 * 60;
27284
- /**
27285
- * Weekly cadence — the epoch schedule is fixed weekly (Tue → Thu UTC on Base).
27286
- * Used only to roll a cycle forward when a request lands inside a window that
27287
- * has already closed.
27288
- */
27289
- const WEEK_SECONDS = 7 * 24 * 60 * 60;
27290
- /**
27291
- * Which phase of the weekly cycle `nowSeconds` falls in, and how long is left
27292
- * of it.
27293
- *
27294
- * `'unknown'` when no epoch boundary has been loaded yet, or when the one on
27295
- * hand is stale (it has already elapsed — the on-chain value refetches to the
27296
- * next boundary shortly after rollover). A caller must render its "no cycle
27297
- * loaded" state there, never a zeroed countdown.
27298
- */
27299
- function computeSettlementWindow({ nowSeconds, nextEpochStart, clearingWindowSeconds = CLEARING_WINDOW_SECONDS, }) {
27300
- // No epoch boundary loaded yet, or a stale one that already elapsed.
27301
- if (nextEpochStart <= 0 || nextEpochStart <= nowSeconds) {
27302
- return { phase: 'unknown' };
27303
- }
27304
- const clearingStart = nextEpochStart - clearingWindowSeconds;
27305
- if (nowSeconds < clearingStart) {
27306
- return {
27307
- phase: 'awaiting',
27308
- secondsUntilClearing: clearingStart - nowSeconds,
27309
- nextClearingStart: clearingStart,
27310
- };
27311
- }
27312
- // nowSeconds is in [clearingStart, nextEpochStart) — inside the window.
27313
- return {
27314
- phase: 'clearing',
27315
- secondsUntilEpochEnd: nextEpochStart - nowSeconds,
27316
- epochEnd: nextEpochStart,
27317
- };
27318
- }
27319
- /**
27320
- * The next cycle boundary strictly after `nowSeconds`, in unix seconds — the
27321
- * cycle close (`nextEpochStart − 48h`) while the window is still open, the
27322
- * epoch end once we are inside it.
27323
- *
27324
- * `undefined` when there is no boundary left to wait for: no epoch boundary
27325
- * loaded, or a cached one that has already elapsed — the same staleness rule
27326
- * `computeSettlementWindow` applies before it reports `'unknown'`.
27327
- *
27328
- * Split out of the state machine because some consumers need the INSTANT
27329
- * rather than the phase: one to flush the cycle-dependent caches when the
27330
- * clock crosses it, one to move a pre-commit screen's snapshot clock at the
27331
- * same moment.
27332
- */
27333
- function nextCycleBoundary(nextEpochStart, nowSeconds, clearingWindowSeconds = CLEARING_WINDOW_SECONDS) {
27334
- if (!nextEpochStart || nextEpochStart <= 0)
27335
- return undefined;
27336
- const clearingStart = nextEpochStart - clearingWindowSeconds;
27337
- if (nowSeconds < clearingStart)
27338
- return clearingStart;
27339
- if (nowSeconds < nextEpochStart)
27340
- return nextEpochStart;
27341
- return undefined;
27342
- }
27343
28768
  /**
27344
- * The cycle-close and outcome dates for a request submitted `now`.
27345
- *
27346
- * - The cycle "closes" (stops accepting requests, starts processing) at the
27347
- * start of the 48h clearing window, i.e. 48h before the epoch end.
27348
- * - Processing takes up to 48h, so the outcome is confirmed by the epoch end
27349
- * (close + 48h).
27350
- *
27351
- * Returns `null` when the epoch boundary is not available or is stale — a
27352
- * caller then omits the dates entirely (omit, don't stub).
27353
- *
27354
- * The common case is a request submitted OUTSIDE the clearing window: the
27355
- * close is `nextEpochStart − 48h` and the outcome is `nextEpochStart`. When
27356
- * the request lands INSIDE the current clearing window (that close is already
27357
- * in the past), it queues for the NEXT weekly cycle, so the close is advanced
27358
- * by whole weeks until it is in the future.
28769
+ * The strategy's region — `'Australia'` for the Apxium family, `'Global'` for
28770
+ * PayFi — derived from its credit originator, exactly as every surface that
28771
+ * states a region must derive it.
28772
+ *
28773
+ * LOAD-BEARING ON THE MONEY PATH: this is the `region` a lender's wallet signs
28774
+ * in the loan-agreement message (`buildLoanAgreementSignMessage`). The backend
28775
+ * rebuilds that message from whatever region the client sends and treats it
28776
+ * as display-only, so nothing but this function keeps the signed word equal to
28777
+ * the word on screen. Never state a region from a constant — that is how a
28778
+ * PayFi lender came to pick "Global · Junior" and sign "Australia".
28779
+ *
28780
+ * Replace the inference with a per-pool field (in the SDK) when one ships.
27359
28781
  */
27360
- function deriveCycleDates(nextEpochStart, nowSeconds) {
27361
- // No boundary loaded, or a stale one that already elapsed — the same
27362
- // staleness rule as `computeSettlementWindow`.
27363
- if (!nextEpochStart || nextEpochStart <= nowSeconds)
27364
- return null;
27365
- let close = nextEpochStart - CLEARING_WINDOW_SECONDS;
27366
- let outcome = nextEpochStart;
27367
- // Inside the current clearing window the close already passed; a request
27368
- // now is queued for the next weekly cycle.
27369
- while (close <= nowSeconds) {
27370
- close += WEEK_SECONDS;
27371
- outcome += WEEK_SECONDS;
27372
- }
27373
- return { close, outcome };
28782
+ function getStrategyRegion(pool) {
28783
+ return getCreditOriginator(pool).region;
27374
28784
  }
27375
28785
 
27376
28786
  // Business rename (2026-07): Apxium strategies market their top retail tranche
@@ -27402,4 +28812,155 @@ function getTrancheDisplayName(trancheName, pool) {
27402
28812
  : trancheName;
27403
28813
  }
27404
28814
 
27405
- export { APXIUM, AU_ALPHA3, AU_MIN_CUMULATIVE_BY_STABLE, BatchRevertedError, CHAIN_CONFIGS, CLEARING_WINDOW_SECONDS, CONTRACT_TTL_MS, DepositFlow, DepositsFacade, EPOCHS_IN_YEAR, Flow, FlowStore, FlowsFacade, INVALID_ACCEPTANCE_SIGNATURE_MESSAGE, INVALID_AUTH_SIGNATURE_MESSAGE, INVOICEMATE, Kasu, KasuSdk, MAX_LENDING_AMOUNT_FALLBACK, MIN_LENDING_AMOUNT_FALLBACK, MIN_TRANCHE_CAPACITY, NO_DIRECTUS_URL_MESSAGE, NO_KYC_PARAMS_MESSAGE, NO_SPENDER_MESSAGE, PortfolioFacade, READ_ONLY_MESSAGE, RIXON_CAPITAL, SdkConfig, StrategiesFacade, UPPER_MEZZANINE, WithdrawFlow, apyToEpochRate, asContractType, auMinimumRemaining, auThresholdFor, buildContractVersionType, buildFullNameRequestMessage, buildLegacyContractRequestMessage, buildLoanAgreementSignMessage, ceilToCents, classifyWalletFailure, compareTrancheSeniority, computeSettlementWindow, countSubmissions, decodeRevert, deriveCycleDates, derivePoolStatus, deriveRequestState, encodeDepositData, epochRateToApy, extractRevertData, fetchUnusedPoolIds, firstSubmissionTimestamp, floorToCents, formatSignTimestampUtc, getCreditOriginator, getInstitutionalLender, getTrancheDisplayName, isAuMinimumExempt, isAustralianKyc, isBelowMinimumCapacity, isCycleClosed, isUnpredictableGas, isUserRejected, lastEventTimestamp, maxNetRateCeiling, netEffectiveApy, netTrancheApyBounds, nextCycleBoundary, parseFormattedMessage, parseMinorUnits, parseTrancheBound, pickDefaultTrancheId, pickHighestYieldTranche, poolAllTranchesFull, poolMaxApy, resolveBoundShortcuts, resolveDepositBounds, selectVisiblePools, submissionEvents, trancheApyBounds, trancheHasCapacity, trancheRiskRank };
28815
+ /**
28816
+ * Forward-looking interest on a lender's CURRENT balances — the figures a
28817
+ * portfolio shows as "weekly", "daily" and the moving part of "lifetime"
28818
+ * interest, and the "≈ $Y a year" line beside a lend amount.
28819
+ *
28820
+ * Every figure is NET of the platform performance fee, the way the chain
28821
+ * credits it: `LendingPool._applyTrancheInterest` mints the lender
28822
+ * `interest × (1 − performanceFee)` at each cycle close, and only that net
28823
+ * amount ever reaches the balance. A gross projection would out-run what the
28824
+ * next close actually credits.
28825
+ *
28826
+ * Units: balances and results are display units of the deployment's stable
28827
+ * asset (not base units); rates are 0..1 fractions; `feePercent` is a
28828
+ * PERCENTAGE IN 0..100 — see `netEffectiveApy`.
28829
+ */
28830
+ /**
28831
+ * Seconds in one epoch's nominal week. The per-epoch rate covers exactly this
28832
+ * window, and the accrual spreads an epoch's interest across it.
28833
+ */
28834
+ const SECONDS_PER_WEEK = 604800;
28835
+ function isUsableFeePercent(feePercent) {
28836
+ return (feePercent !== null &&
28837
+ Number.isFinite(feePercent) &&
28838
+ feePercent >= 0 &&
28839
+ feePercent <= 100);
28840
+ }
28841
+ /**
28842
+ * Interest the slices accrue over ONE epoch, net of the fee.
28843
+ *
28844
+ * ```
28845
+ * weekly = Σ(slice.balance × slice.epochRate) × (1 − feePercent / 100)
28846
+ * ```
28847
+ *
28848
+ * **The rate is the raw per-epoch one, never the APY.** An APY is that rate
28849
+ * already compounded over `EPOCHS_IN_YEAR`; dividing it by 365 charges a day
28850
+ * of compounded annual growth to a single day and overstates it.
28851
+ *
28852
+ * FAILS CLOSED, returning `null` rather than a wrong number:
28853
+ * - a `null`, non-finite or out-of-domain fee — a fee-less projection dressed
28854
+ * up as the lender's net earnings is the failure this guards;
28855
+ * - any slice with a POSITIVE balance and a non-finite rate — skipping it
28856
+ * would return a silently partial total, quietly missing that slice.
28857
+ *
28858
+ * Non-finite and non-positive BALANCES are skipped: a slice with nothing in it
28859
+ * earns nothing whatever its rate, so it cannot hide earnings. A portfolio
28860
+ * with nothing lent is therefore a legitimate `0`, never `null`.
28861
+ */
28862
+ function weeklyInterest(slices, feePercent) {
28863
+ if (!isUsableFeePercent(feePercent))
28864
+ return null;
28865
+ let gross = 0;
28866
+ for (const slice of slices) {
28867
+ if (!Number.isFinite(slice.balance) || slice.balance <= 0)
28868
+ continue;
28869
+ if (!Number.isFinite(slice.epochRate))
28870
+ return null;
28871
+ gross += slice.balance * slice.epochRate;
28872
+ }
28873
+ return gross * (1 - feePercent / 100);
28874
+ }
28875
+ /**
28876
+ * A day's share of the epoch's net interest: a seventh of it, NOT a
28877
+ * de-compounded daily rate. The chain mints interest once per epoch, so
28878
+ * nothing compounds within the cycle and the accrual is linear across it.
28879
+ *
28880
+ * Fails closed with its input: `null` or non-finite → `null`.
28881
+ */
28882
+ function dailyInterest(weekly) {
28883
+ if (weekly === null || !Number.isFinite(weekly))
28884
+ return null;
28885
+ return weekly / 7;
28886
+ }
28887
+ /**
28888
+ * Interest accrued since each pool's OWN last cycle close — the moving part
28889
+ * of lifetime interest (lifetime = settled on chain + this).
28890
+ *
28891
+ * ```
28892
+ * accrued = Σ_pool weekly_pool × clamp(now − clearing_pool, 0, 604 800) / 604 800
28893
+ * ```
28894
+ *
28895
+ * **Per pool, because pools clear independently.** One shared anchor accrues
28896
+ * a stale pool's share from a close it never had, and when another pool
28897
+ * clears it drops the total by the stale pool's whole weekly share. Anchored
28898
+ * per pool, the total is monotonic across any single pool's clearing: that
28899
+ * pool swaps its accrued share for a capitalised one, and no other term moves.
28900
+ *
28901
+ * **The one-epoch cap is load-bearing.** Clearings are not exactly weekly,
28902
+ * and `weeklyInterest` is per EPOCH, so extrapolating past seven days would
28903
+ * promise interest the close will not credit; a late clearing under-reports
28904
+ * instead. The low clamp makes a close ahead of the local clock zero, never
28905
+ * negative.
28906
+ *
28907
+ * A non-finite term contributes nothing; a non-finite `nowSeconds` accrues
28908
+ * nothing. Callers fail closed upstream by not offering an accrual for a pool
28909
+ * whose weekly interest or anchor is unknown.
28910
+ */
28911
+ function accruedInterest(accruals, nowSeconds) {
28912
+ if (!Number.isFinite(nowSeconds))
28913
+ return 0;
28914
+ let accrued = 0;
28915
+ for (const { weeklyInterest: weekly, clearingTimestamp } of accruals) {
28916
+ if (!Number.isFinite(weekly) || !Number.isFinite(clearingTimestamp)) {
28917
+ continue;
28918
+ }
28919
+ const elapsed = Math.min(Math.max(nowSeconds - clearingTimestamp, 0), SECONDS_PER_WEEK);
28920
+ accrued += (weekly * elapsed) / SECONDS_PER_WEEK;
28921
+ }
28922
+ return accrued;
28923
+ }
28924
+ /**
28925
+ * What `amount` earns in a year at a NET rate — the "≈ $Y a year" line.
28926
+ * Feed it `netEffectiveApy(...)`, never a gross APY: the result is a lender's
28927
+ * earnings, so the fee must already be off the rate.
28928
+ *
28929
+ * FAILS CLOSED: a non-finite or negative amount or rate returns `NaN`, which
28930
+ * the consumer renders as "no figure" — exactly what `netEffectiveApy`
28931
+ * returns when the fee is unknown, so that state carries straight through.
28932
+ *
28933
+ * @param amount display units of the stable asset.
28934
+ * @param netApy the net effective annual rate as a 0..1 fraction.
28935
+ */
28936
+ function projectedAnnualEarnings(amount, netApy) {
28937
+ if (!Number.isFinite(amount) || amount < 0)
28938
+ return NaN;
28939
+ if (!Number.isFinite(netApy) || netApy < 0)
28940
+ return NaN;
28941
+ return amount * netApy;
28942
+ }
28943
+ /**
28944
+ * Balance-weighted average APY across slices, skipping non-finite and
28945
+ * non-positive balances and non-finite rates; `0` when nothing carries a
28946
+ * positive balance. Whatever basis goes in comes out — feed it NET rates for
28947
+ * an average effective (net) rate.
28948
+ */
28949
+ function weightedAverageApy(slices) {
28950
+ let totalBalance = 0;
28951
+ let weighted = 0;
28952
+ for (const slice of slices) {
28953
+ if (!Number.isFinite(slice.balance) || !Number.isFinite(slice.apy)) {
28954
+ continue;
28955
+ }
28956
+ if (slice.balance <= 0)
28957
+ continue;
28958
+ totalBalance += slice.balance;
28959
+ weighted += slice.balance * slice.apy;
28960
+ }
28961
+ if (totalBalance <= 0)
28962
+ return 0;
28963
+ return weighted / totalBalance;
28964
+ }
28965
+
28966
+ export { APXIUM, AU_ALPHA3, AU_MIN_CUMULATIVE_BY_STABLE, BatchPendingError, BatchRevertedError, BatchSubmissionError, CHAIN_CONFIGS, CLEARING_WINDOW_SECONDS, CONTRACT_TTL_MS, CallsFacade, DAYS_PER_EPOCH, DAYS_PER_MONTH, DEFAULT_BATCH_POLLING_POLICY, DepositBatchMismatchError, DepositFlow, DepositsFacade, ENTRY_POINT_V07, EPOCHS_IN_YEAR, Flow, FlowStore, FlowsFacade, INVALID_ACCEPTANCE_SIGNATURE_MESSAGE, INVALID_AUTH_SIGNATURE_MESSAGE, INVOICEMATE, Kasu, KasuSdk, MAX_LENDING_AMOUNT_FALLBACK, MIN_DAYS_FOR_MONTHS, MIN_LENDING_AMOUNT_FALLBACK, MIN_TRANCHE_CAPACITY, NOT_A_WITHDRAWAL_CALL_MESSAGE, NO_DIRECTUS_URL_MESSAGE, NO_KYC_PARAMS_MESSAGE, NO_SPENDER_MESSAGE, NO_WITHDRAWAL_ENCODER_MESSAGE, PortfolioFacade, READ_ONLY_MESSAGE, RIXON_CAPITAL, SECONDS_PER_WEEK, SPONSORED_CALL_KINDS, SPONSORED_CALL_SELECTORS, SdkConfig, SponsoredCallInputError, StrategiesFacade, UPPER_MEZZANINE, WithdrawFlow, accruedInterest, apyToEpochRate, asContractType, auMinimumRemaining, auThresholdFor, availableFixedTermConfigs, batchOutcomeOf, buildContractVersionType, buildFullNameRequestMessage, buildLegacyContractRequestMessage, buildLoanAgreementSignMessage, cancelDeadline, ceilToCents, checkBatchOutcome, classifyWalletFailure, compareTrancheSeniority, computeSettlementWindow, countSubmissions, dailyInterest, decodeRevert, deriveCycleDates, derivePoolStatus, deriveRequestState, encodeCancelDepositCall, encodeCancelWithdrawalCall, encodeDepositData, encodeRequestWithdrawalCall, encodeWithdrawAtExpiryCall, epochRateToApy, explorerAddressUrl, explorerTxUrl, extractRevertData, fetchUnusedPoolIds, firstSubmissionTimestamp, floorToCents, formatSignTimestampUtc, getCreditOriginator, getInstitutionalLender, getStrategyRegion, getTrancheDisplayName, isAuMinimumExempt, isAustralianKyc, isBelowMinimumCapacity, isCycleClosed, isDefinitivelyNotSubmitted, isFixedTermConfigId, isUnpredictableGas, isUserRejected, lastEventTimestamp, lockDurationDays, lockDurationMonths, maxNetRateCeiling, netEffectiveApy, netTrancheApyBounds, nextCycleBoundary, parseFormattedMessage, parseMinorUnits, parseTrancheBound, pickDefaultTrancheId, pickHighestYieldTranche, poolAllTranchesFull, poolMaxApy, projectedAnnualEarnings, proveDepositInReceipt, proveUserOperationInReceipt, resolveBoundShortcuts, resolveDepositBounds, selectVisiblePools, submissionEvents, submitBatch, submitSponsoredCall, trancheApyBounds, trancheHasCapacity, trancheRiskRank, validateDepositBatchCalls, weeklyInterest, weightedAverageApy };