@kasufinance/kasu-sdk 2.7.1 → 2.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.md +162 -7
  2. package/dist/bundle.cjs.js +2292 -449
  3. package/dist/bundle.esm.js +2253 -451
  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 +40 -3
  34. package/dist/facade/flows.js +36 -3
  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 -4
  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 +128 -4
  53. package/dist/flows/deposit-flow.js +233 -4
  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 +3 -1
  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/UserLending/user-lending.d.ts +13 -0
  64. package/dist/services/UserLending/user-lending.js +25 -4
  65. package/dist/services/UserLending/user-lending.js.map +1 -1
  66. package/package.json +1 -1
  67. package/src/domain/batch-proof.ts +576 -0
  68. package/src/domain/explorer-urls.ts +74 -0
  69. package/src/domain/fixed-term.ts +115 -0
  70. package/src/domain/index.ts +56 -0
  71. package/src/domain/partners.ts +35 -0
  72. package/src/domain/requests.ts +37 -0
  73. package/src/domain/sponsored-calls.ts +213 -0
  74. package/src/domain/yields.ts +189 -0
  75. package/src/facade/calls.ts +135 -0
  76. package/src/facade/chain-configs.ts +5 -0
  77. package/src/facade/flows.ts +35 -2
  78. package/src/facade/index.ts +3 -0
  79. package/src/facade/kasu.ts +32 -2
  80. package/src/facade/strategies.ts +98 -7
  81. package/src/facade/types.ts +19 -0
  82. package/src/flows/batch-errors.ts +118 -0
  83. package/src/flows/batch-submission.ts +511 -0
  84. package/src/flows/deposit-flow.ts +379 -7
  85. package/src/flows/index.ts +34 -1
  86. package/src/flows/withdraw-flow.ts +219 -10
  87. package/src/services/DataService/queries.ts +1 -0
  88. package/src/services/UserLending/user-lending.ts +38 -10
@@ -2033,7 +2033,7 @@ function isSource(source) {
2033
2033
  * Given a GraphQL source, parses it into a Document.
2034
2034
  * Throws GraphQLError if a syntax error is encountered.
2035
2035
  */
2036
- function parse(source, options) {
2036
+ function parse$1(source, options) {
2037
2037
  const parser = new Parser(source, options);
2038
2038
  const document = parser.parseDocument();
2039
2039
  Object.defineProperty(document, 'tokenCount', {
@@ -4296,7 +4296,7 @@ const resolveRequestDocument = (document) => {
4296
4296
  if (typeof document === `string`) {
4297
4297
  let operationName = undefined;
4298
4298
  try {
4299
- const parsedDocument = parse(document);
4299
+ const parsedDocument = parse$1(document);
4300
4300
  operationName = extractOperationName(parsedDocument);
4301
4301
  }
4302
4302
  catch (err) {
@@ -15811,6 +15811,7 @@ const getPoolOverviewQuery = (ids) => gql `
15811
15811
  fixedTermDepositStatus
15812
15812
  fixedTermDepositAllowlist{
15813
15813
  id
15814
+ isAllowlisted
15814
15815
  user{
15815
15816
  id
15816
15817
  }
@@ -17240,10 +17241,32 @@ class UserLending {
17240
17241
  return yield this._lendingPoolManagerAbi.requestWithdrawal(lendingPool, tranche, amount);
17241
17242
  });
17242
17243
  }
17243
- requestWithdrawalInAsset(lendingPool, tranche, assetAmount) {
17244
+ /**
17245
+ * The tranche shares a withdrawal of `assetAmount` requests — the
17246
+ * tranche's own `convertToShares`, read now. `requestWithdrawalInAsset`
17247
+ * and the sponsored `kasu.calls.requestWithdrawal` both use this, so the
17248
+ * two paths cannot convert differently.
17249
+ */
17250
+ getWithdrawalSharesForAssets(tranche, assetAmount) {
17251
+ return __awaiter(this, void 0, void 0, function* () {
17252
+ const trancheContract = ILendingPoolTrancheAbi__factory.connect(tranche, this._signerOrProvider);
17253
+ return yield trancheContract.convertToShares(assetAmount);
17254
+ });
17255
+ }
17256
+ /**
17257
+ * Every tranche share `user` holds — what a max withdrawal requests
17258
+ * (`balanceOf(user)` on the tranche, read now). Shared by
17259
+ * `requestWithdrawalMax` and the sponsored `kasu.calls.requestWithdrawal`.
17260
+ */
17261
+ getWithdrawalSharesMax(tranche, user) {
17244
17262
  return __awaiter(this, void 0, void 0, function* () {
17245
17263
  const trancheContract = ILendingPoolTrancheAbi__factory.connect(tranche, this._signerOrProvider);
17246
- const shareAmount = yield trancheContract.convertToShares(assetAmount);
17264
+ return yield trancheContract['balanceOf(address)'](user);
17265
+ });
17266
+ }
17267
+ requestWithdrawalInAsset(lendingPool, tranche, assetAmount) {
17268
+ return __awaiter(this, void 0, void 0, function* () {
17269
+ const shareAmount = yield this.getWithdrawalSharesForAssets(tranche, assetAmount);
17247
17270
  return yield this._lendingPoolManagerAbi.requestWithdrawal(lendingPool, tranche, shareAmount);
17248
17271
  });
17249
17272
  }
@@ -17255,8 +17278,7 @@ class UserLending {
17255
17278
  }
17256
17279
  requestWithdrawalMax(lendingPool, tranche, user) {
17257
17280
  return __awaiter(this, void 0, void 0, function* () {
17258
- const trancheContract = ILendingPoolTrancheAbi__factory.connect(tranche, this._signerOrProvider);
17259
- const userShares = yield trancheContract['balanceOf(address)'](user);
17281
+ const userShares = yield this.getWithdrawalSharesMax(tranche, user);
17260
17282
  return yield this._lendingPoolManagerAbi.requestWithdrawal(lendingPool, tranche, userShares);
17261
17283
  });
17262
17284
  }
@@ -20462,7 +20484,7 @@ function toFormData$1(obj, formData, options) {
20462
20484
  *
20463
20485
  * @returns {string} The encoded string.
20464
20486
  */
20465
- function encode$1(str) {
20487
+ function encode$2(str) {
20466
20488
  const charMap = {
20467
20489
  '!': '%21',
20468
20490
  "'": '%27',
@@ -20498,8 +20520,8 @@ prototype.append = function append(name, value) {
20498
20520
 
20499
20521
  prototype.toString = function toString(encoder) {
20500
20522
  const _encode = encoder
20501
- ? (value) => encoder.call(this, value, encode$1)
20502
- : encode$1;
20523
+ ? (value) => encoder.call(this, value, encode$2)
20524
+ : encode$2;
20503
20525
 
20504
20526
  return this._pairs
20505
20527
  .map(function each(pair) {
@@ -20516,7 +20538,7 @@ prototype.toString = function toString(encoder) {
20516
20538
  *
20517
20539
  * @returns {string} The encoded value.
20518
20540
  */
20519
- function encode(val) {
20541
+ function encode$1(val) {
20520
20542
  return encodeURIComponent(val)
20521
20543
  .replace(/%3A/gi, ':')
20522
20544
  .replace(/%24/g, '$')
@@ -20548,7 +20570,7 @@ function buildURL(url, params, options) {
20548
20570
  // Read serializer options pollution-safely: own properties and methods on a
20549
20571
  // class/template prototype are honored, but values injected onto a polluted
20550
20572
  // Object.prototype are ignored.
20551
- const _encode = utils$1.getSafeProp(_options, 'encode') || encode;
20573
+ const _encode = utils$1.getSafeProp(_options, 'encode') || encode$1;
20552
20574
  const serializeFn = utils$1.getSafeProp(_options, 'serialize');
20553
20575
 
20554
20576
  let serializedParams;
@@ -21188,7 +21210,7 @@ let CanceledError$1 = class CanceledError extends AxiosError$1 {
21188
21210
  *
21189
21211
  * @returns {object} The response.
21190
21212
  */
21191
- function settle(resolve, reject, response) {
21213
+ function settle$1(resolve, reject, response) {
21192
21214
  const validateStatus = response.config.validateStatus;
21193
21215
  if (!response.status || !validateStatus || validateStatus(response.status)) {
21194
21216
  resolve(response);
@@ -21951,7 +21973,7 @@ var xhrAdapter = isXHRAdapterSupported &&
21951
21973
  request,
21952
21974
  };
21953
21975
 
21954
- settle(
21976
+ settle$1(
21955
21977
  function _resolve(value) {
21956
21978
  resolve(value);
21957
21979
  done();
@@ -23009,7 +23031,7 @@ const factory = (env) => {
23009
23031
  !isStreamResponse && unsubscribe && unsubscribe();
23010
23032
 
23011
23033
  return await new Promise((resolve, reject) => {
23012
- settle(resolve, reject, {
23034
+ settle$1(resolve, reject, {
23013
23035
  data: responseData,
23014
23036
  headers: AxiosHeaders$1.from(response.headers),
23015
23037
  status: response.status,
@@ -24185,6 +24207,238 @@ class SdkConfig {
24185
24207
  }
24186
24208
  }
24187
24209
 
24210
+ /**
24211
+ * The single-call `LendingPoolManager` requests a sponsored relay may carry,
24212
+ * encoded by the SDK so no application hand-builds calldata or hand-maps the
24213
+ * relay's `kind` for them.
24214
+ *
24215
+ * Every function here is PURE: addresses and ids in, `{ kind, call }` out.
24216
+ * Reading the chain (the shares a withdrawal is for) is the facade's job —
24217
+ * `kasu.calls` — and sending is the application's.
24218
+ *
24219
+ * Each call is `{ to, data }` and nothing else: no `value` (none of these
24220
+ * functions is payable), no `from` (the manager acts for `msg.sender`, the
24221
+ * lender's own account, which is why none of them takes a user argument).
24222
+ * `data` is ethers' ABI encoding, which is canonical by construction — the
24223
+ * relay re-encodes what it decodes and refuses anything that differs, so a
24224
+ * hand-tweaked blob never gets through, and an SDK-built one always matches.
24225
+ */
24226
+ /**
24227
+ * The relay's name for each shape. Carried beside the call so the app posts
24228
+ * `kind` from the SDK, never from a table of its own — a request declaring the
24229
+ * wrong kind is refused (`invalid_calls`), so a hand-mapped table that drifts
24230
+ * turns into a lender who cannot withdraw.
24231
+ */
24232
+ const SPONSORED_CALL_KINDS = [
24233
+ /** `LendingPoolManager.requestWithdrawal(pool, tranche, shares)`. */
24234
+ 'withdrawal',
24235
+ /** `LendingPoolManager.cancelDepositRequest(pool, dNftID)`. */
24236
+ 'cancel_deposit',
24237
+ /** `LendingPoolManager.cancelWithdrawalRequest(pool, wNftID)`. */
24238
+ 'cancel_withdrawal',
24239
+ /** `LendingPoolManager.requestFixedTermDepositWithdrawal(pool, lockId)`. */
24240
+ 'withdraw_at_expiry',
24241
+ ];
24242
+ /**
24243
+ * Widened to the plain `Interface`: the typechain one overloads
24244
+ * `encodeFunctionData` per literal name, and the name here comes from
24245
+ * `FUNCTION_OF`.
24246
+ */
24247
+ const LENDING_POOL_MANAGER$2 = ILendingPoolManagerAbi__factory.createInterface();
24248
+ /** The `LendingPoolManager` function behind each kind. */
24249
+ const FUNCTION_OF = Object.freeze({
24250
+ withdrawal: 'requestWithdrawal',
24251
+ cancel_deposit: 'cancelDepositRequest',
24252
+ cancel_withdrawal: 'cancelWithdrawalRequest',
24253
+ withdraw_at_expiry: 'requestFixedTermDepositWithdrawal',
24254
+ });
24255
+ /**
24256
+ * The 4-byte selector of each kind's function, DERIVED from the generated
24257
+ * `ILendingPoolManager` ABI — never typed in by hand, so an ABI change moves
24258
+ * it and the cross-check spec against the relay's own pins goes red.
24259
+ */
24260
+ const SPONSORED_CALL_SELECTORS = Object.freeze({
24261
+ withdrawal: LENDING_POOL_MANAGER$2.getSighash(FUNCTION_OF.withdrawal),
24262
+ cancel_deposit: LENDING_POOL_MANAGER$2.getSighash(FUNCTION_OF.cancel_deposit),
24263
+ cancel_withdrawal: LENDING_POOL_MANAGER$2.getSighash(FUNCTION_OF.cancel_withdrawal),
24264
+ withdraw_at_expiry: LENDING_POOL_MANAGER$2.getSighash(FUNCTION_OF.withdraw_at_expiry),
24265
+ });
24266
+ /**
24267
+ * What an encoder throws for an input it will not encode. `code` names the
24268
+ * argument; the message names no party, so a wallet-rejection classifier can
24269
+ * never read it as something the lender did. Nothing has been sent when this
24270
+ * is thrown.
24271
+ */
24272
+ class SponsoredCallInputError extends Error {
24273
+ constructor(code) {
24274
+ super(`Sponsored call input refused (${code})`);
24275
+ this.name = 'SponsoredCallInputError';
24276
+ this.code = code;
24277
+ Object.setPrototypeOf(this, SponsoredCallInputError.prototype);
24278
+ }
24279
+ }
24280
+ const ADDRESS_RE$2 = /^0x[0-9a-fA-F]{40}$/;
24281
+ function assertAddress(value, code) {
24282
+ if (typeof value !== 'string' || !ADDRESS_RE$2.test(value)) {
24283
+ throw new SponsoredCallInputError(code);
24284
+ }
24285
+ }
24286
+ /** A uint256 — an NFT or lock id. Zero is a valid id as far as encoding goes. */
24287
+ function toUint256(value, code) {
24288
+ let parsed;
24289
+ try {
24290
+ parsed = ethers.BigNumber.from(value);
24291
+ }
24292
+ catch (_a) {
24293
+ throw new SponsoredCallInputError(code);
24294
+ }
24295
+ if (parsed.isNegative() || parsed.gt(ethers.constants.MaxUint256)) {
24296
+ throw new SponsoredCallInputError(code);
24297
+ }
24298
+ return parsed;
24299
+ }
24300
+ function encode(kind, lendingPoolManager, args) {
24301
+ return {
24302
+ kind,
24303
+ call: {
24304
+ to: lendingPoolManager,
24305
+ data: LENDING_POOL_MANAGER$2.encodeFunctionData(FUNCTION_OF[kind], args),
24306
+ },
24307
+ };
24308
+ }
24309
+ /**
24310
+ * `requestWithdrawal(poolId, trancheId, shares)` — `withdrawal`.
24311
+ *
24312
+ * `shares` are tranche SHARES, not assets: `kasu.calls.requestWithdrawal`
24313
+ * converts an asset amount (or `'max'`) into them exactly as
24314
+ * `UserLending.requestWithdrawalInAsset` / `requestWithdrawalMax` do. Zero
24315
+ * shares is refused here: the contract reverts on it
24316
+ * (`RequestWithdrawSharesAreZero`) and the relay refuses it before that.
24317
+ */
24318
+ function encodeRequestWithdrawalCall(lendingPoolManager, poolId, trancheId, shares) {
24319
+ assertAddress(lendingPoolManager, 'lending-pool-manager');
24320
+ assertAddress(poolId, 'pool');
24321
+ assertAddress(trancheId, 'tranche');
24322
+ const amount = toUint256(shares, 'shares');
24323
+ if (amount.isZero())
24324
+ throw new SponsoredCallInputError('zero-shares');
24325
+ return encode('withdrawal', lendingPoolManager, [
24326
+ poolId,
24327
+ trancheId,
24328
+ amount,
24329
+ ]);
24330
+ }
24331
+ /** `cancelDepositRequest(poolId, dNftId)` — `cancel_deposit`. */
24332
+ function encodeCancelDepositCall(lendingPoolManager, poolId, dNftId) {
24333
+ assertAddress(lendingPoolManager, 'lending-pool-manager');
24334
+ assertAddress(poolId, 'pool');
24335
+ return encode('cancel_deposit', lendingPoolManager, [
24336
+ poolId,
24337
+ toUint256(dNftId, 'nft-id'),
24338
+ ]);
24339
+ }
24340
+ /** `cancelWithdrawalRequest(poolId, wNftId)` — `cancel_withdrawal`. */
24341
+ function encodeCancelWithdrawalCall(lendingPoolManager, poolId, wNftId) {
24342
+ assertAddress(lendingPoolManager, 'lending-pool-manager');
24343
+ assertAddress(poolId, 'pool');
24344
+ return encode('cancel_withdrawal', lendingPoolManager, [
24345
+ poolId,
24346
+ toUint256(wNftId, 'nft-id'),
24347
+ ]);
24348
+ }
24349
+ /**
24350
+ * `requestFixedTermDepositWithdrawal(poolId, fixedTermDepositId)` —
24351
+ * `withdraw_at_expiry`. The contract function takes the pool and the LOCK id
24352
+ * only (no tranche): the manager forwards to the fixed-term contract on the
24353
+ * lender's behalf, which knows the lock's tranche.
24354
+ */
24355
+ function encodeWithdrawAtExpiryCall(lendingPoolManager, poolId, fixedTermDepositId) {
24356
+ assertAddress(lendingPoolManager, 'lending-pool-manager');
24357
+ assertAddress(poolId, 'pool');
24358
+ return encode('withdraw_at_expiry', lendingPoolManager, [
24359
+ poolId,
24360
+ toUint256(fixedTermDepositId, 'lock-id'),
24361
+ ]);
24362
+ }
24363
+
24364
+ const ADDRESS_RE$1 = /^0x[0-9a-fA-F]{40}$/;
24365
+ /**
24366
+ * Encoded, ready-to-sponsor `LendingPoolManager` calls — read, then encode,
24367
+ * NEVER send. Each returns `{ kind, call }`: post `kind` and `[call]` to a
24368
+ * sponsored relay (or `submitSponsoredCall`), unchanged.
24369
+ *
24370
+ * ```ts
24371
+ * const { kind, call } = kasu.calls.cancelDepositRequest(poolId, dNftId);
24372
+ * ```
24373
+ *
24374
+ * `call.to` is always THIS chain's `LendingPoolManager`, from the chain
24375
+ * config — the same address the deposit flow approves and calls — so no app
24376
+ * supplies the one argument whose wrong value the relay refuses outright.
24377
+ *
24378
+ * Works on a read-only instance: the only chain access is the share read a
24379
+ * withdrawal needs, and nothing is signed or sent here.
24380
+ */
24381
+ class CallsFacade {
24382
+ constructor(_userLending, _lendingPoolManager) {
24383
+ this._userLending = _userLending;
24384
+ this._lendingPoolManager = _lendingPoolManager;
24385
+ }
24386
+ /**
24387
+ * `requestWithdrawal(pool, tranche, shares)` for an asset amount or
24388
+ * `'max'`, converted into shares exactly as the direct path does it
24389
+ * (`UserLending.requestWithdrawalInAsset` / `requestWithdrawalMax`): the
24390
+ * tranche's `convertToShares(amount)`, or the lender's whole
24391
+ * `balanceOf(userAddress)`, read now.
24392
+ *
24393
+ * Rejects with `SponsoredCallInputError` — before anything is sent — for
24394
+ * a non-positive amount, a malformed address, or a conversion that comes
24395
+ * to ZERO shares (an empty position, or dust below one share), which the
24396
+ * contract would revert and the relay refuses.
24397
+ */
24398
+ requestWithdrawal(params) {
24399
+ return __awaiter(this, void 0, void 0, function* () {
24400
+ const { poolId, trancheId, amount, userAddress } = params;
24401
+ if (typeof userAddress !== 'string' || !ADDRESS_RE$1.test(userAddress)) {
24402
+ throw new SponsoredCallInputError('user');
24403
+ }
24404
+ if (typeof poolId !== 'string' || !ADDRESS_RE$1.test(poolId)) {
24405
+ throw new SponsoredCallInputError('pool');
24406
+ }
24407
+ if (typeof trancheId !== 'string' || !ADDRESS_RE$1.test(trancheId)) {
24408
+ throw new SponsoredCallInputError('tranche');
24409
+ }
24410
+ let shares;
24411
+ if (amount === 'max') {
24412
+ // Lowercased, as `WithdrawFlow` hands `withdrawMax` the address.
24413
+ shares = yield this._userLending.getWithdrawalSharesMax(trancheId, userAddress.toLowerCase());
24414
+ }
24415
+ else {
24416
+ if (!ethers.BigNumber.isBigNumber(amount) || amount.lte(0)) {
24417
+ throw new SponsoredCallInputError('non-positive-amount');
24418
+ }
24419
+ shares = yield this._userLending.getWithdrawalSharesForAssets(trancheId, amount);
24420
+ }
24421
+ return encodeRequestWithdrawalCall(this._lendingPoolManager, poolId, trancheId, shares);
24422
+ });
24423
+ }
24424
+ /** `cancelDepositRequest(pool, dNftId)` — the pending deposit's NFT id. */
24425
+ cancelDepositRequest(poolId, dNftId) {
24426
+ return encodeCancelDepositCall(this._lendingPoolManager, poolId, dNftId);
24427
+ }
24428
+ /** `cancelWithdrawalRequest(pool, wNftId)` — the pending withdrawal's NFT id. */
24429
+ cancelWithdrawalRequest(poolId, wNftId) {
24430
+ return encodeCancelWithdrawalCall(this._lendingPoolManager, poolId, wNftId);
24431
+ }
24432
+ /**
24433
+ * `requestFixedTermDepositWithdrawal(pool, fixedTermDepositId)` — what
24434
+ * `UserLending.withdrawAtExpiry(pool, lockId)` sends: the pool and the
24435
+ * LOCK id, no tranche.
24436
+ */
24437
+ withdrawAtExpiry(poolId, fixedTermDepositId) {
24438
+ return encodeWithdrawAtExpiryCall(this._lendingPoolManager, poolId, fixedTermDepositId);
24439
+ }
24440
+ }
24441
+
24188
24442
  /**
24189
24443
  * Browser-viable XDC RPCs only. `rpc.xdc.org` / `erpc.xdc.org` answer OPTIONS
24190
24444
  * without `access-control-allow-origin`, so a browser call hangs on preflight
@@ -24242,6 +24496,7 @@ const CHAIN_CONFIGS = {
24242
24496
  'https://base-rpc.publicnode.com',
24243
24497
  'https://mainnet.base.org',
24244
24498
  ],
24499
+ blockExplorerUrl: 'https://basescan.org',
24245
24500
  },
24246
24501
  xdc: {
24247
24502
  chainId: 50,
@@ -24277,6 +24532,7 @@ const CHAIN_CONFIGS = {
24277
24532
  currencyCode: 'AUD',
24278
24533
  },
24279
24534
  rpcUrls: XDC_RPC_URLS,
24535
+ blockExplorerUrl: 'https://xdcscan.com',
24280
24536
  },
24281
24537
  'xdc-usdc': {
24282
24538
  chainId: 50,
@@ -24308,6 +24564,7 @@ const CHAIN_CONFIGS = {
24308
24564
  currencyCode: 'USD',
24309
24565
  },
24310
24566
  rpcUrls: XDC_RPC_URLS,
24567
+ blockExplorerUrl: 'https://xdcscan.com',
24311
24568
  },
24312
24569
  plume: {
24313
24570
  chainId: 98866,
@@ -24348,6 +24605,8 @@ const CHAIN_CONFIGS = {
24348
24605
  // pointing at an endpoint nobody maintains. Pass your own
24349
24606
  // `signerOrProvider` to read the frozen history.
24350
24607
  rpcUrls: [],
24608
+ // The explorer still serves the frozen history.
24609
+ blockExplorerUrl: 'https://explorer.plume.org',
24351
24610
  retired: true,
24352
24611
  },
24353
24612
  };
@@ -24467,128 +24726,1084 @@ class DepositsFacade {
24467
24726
  }
24468
24727
 
24469
24728
  /**
24470
- * The tiny observable-state kernel both flows are built on.
24729
+ * The errors an atomic-batch submitter throws, in one module both
24730
+ * `DepositFlow` and `submitBatch` import — so `instanceof` is one class
24731
+ * whichever of them a consumer imports it through.
24732
+ */
24733
+ /**
24734
+ * What a `depositBatch` port throws when it KNOWS the batch reverted — from a
24735
+ * simulation before sending, from the submitter refusing to broadcast a batch
24736
+ * that would revert, or from a mined receipt whose user operation reverted.
24737
+ * The run ends `{ step: 'request', reason: 'reverted', revertError }`.
24738
+ *
24739
+ * `revertError` is the custom error the contract named (`'ClearingIsPending'`,
24740
+ * `'UserNotKycd'`, …), exactly as the ABI declares it. Pass `''` when the
24741
+ * batch is known to revert but not why: the consumer then renders its generic
24742
+ * `reverted` copy, which is what an unknown name gets anyway.
24743
+ *
24744
+ * A revert is DEFINITIVE — nothing was deposited. Throw it only when that is
24745
+ * known; anything less certain is a `BatchPendingError`.
24746
+ */
24747
+ class BatchRevertedError extends Error {
24748
+ constructor(revertError, message) {
24749
+ super(message !== null && message !== void 0 ? message : `DepositFlow: the batch reverted${revertError ? ` with ${revertError}` : ''}`);
24750
+ this.name = 'BatchRevertedError';
24751
+ this.revertError = revertError;
24752
+ // Keeps `instanceof` honest under a down-levelled build.
24753
+ Object.setPrototypeOf(this, BatchRevertedError.prototype);
24754
+ }
24755
+ }
24756
+ /**
24757
+ * Every non-revert failure of a batch submission, carrying its outcome and a
24758
+ * short code (`'status_timeout'`, `'receipt_unavailable'`, …).
24471
24759
  *
24472
- * Deliberately not a framework store: no React, no signals, no dependency.
24473
- * A flow is a long asynchronous run that has to publish every transition to
24474
- * whatever is rendering it, and has to be able to abandon a run that the
24475
- * consumer walked away from. That is the whole requirement, and it is 60 lines.
24760
+ * The message is fixed and names no party: the wallet-error classifier reads a
24761
+ * thrown error's text for a USER rejection, and a relay outage worded "…wallet
24762
+ * refused…" must never reach the lender as something they did.
24476
24763
  *
24477
- * The `generation` counter is the abandonment mechanism. `reset()` bumps it;
24478
- * an in-flight run captured the previous value, so every later `patch` it
24479
- * attempts is dropped and every `isCurrent` checkpoint tells it to stop. It is
24480
- * what makes `reset()` safe to call mid-flight — the run cannot resurrect a
24481
- * dead state or, worse, drive the UI back into a phase the consumer has left.
24764
+ * `outcome` has NO default. `failed` is the one outcome that invites a second
24765
+ * deposit, so a port author has to spell it out — a `throw new
24766
+ * BatchSubmissionError(code)` written in a catch block after the POST left
24767
+ * must not quietly become "try again".
24482
24768
  */
24483
- class FlowStore {
24484
- constructor(_initial) {
24485
- this._initial = _initial;
24486
- this._generation = 0;
24487
- this._listeners = new Set();
24488
- this._state = _initial;
24489
- }
24490
- get state() {
24491
- return this._state;
24492
- }
24493
- /** The run token an in-flight pipeline captures at `start()`. */
24494
- get generation() {
24495
- return this._generation;
24496
- }
24497
- /** False once `reset()` (or a later `start()`) has superseded `token`. */
24498
- isCurrent(token) {
24499
- return this._generation === token;
24500
- }
24501
- /**
24502
- * Subscribe to every transition. Returns the unsubscribe function; calling
24503
- * it twice is harmless.
24504
- *
24505
- * Listeners are NOT called on subscribe — read `state` for the current
24506
- * value. A listener that throws is not allowed to break the pipeline or
24507
- * starve the listeners registered after it, so throws are swallowed.
24508
- */
24509
- subscribe(listener) {
24510
- this._listeners.add(listener);
24511
- return () => {
24512
- this._listeners.delete(listener);
24513
- };
24514
- }
24515
- /**
24516
- * Merge `next` into the state and notify, unless `token` names a
24517
- * superseded run.
24518
- */
24519
- patch(next, token) {
24520
- if (token !== undefined && !this.isCurrent(token))
24521
- return;
24522
- this._state = Object.assign(Object.assign({}, this._state), next);
24523
- for (const listener of [...this._listeners]) {
24524
- try {
24525
- listener(this._state);
24526
- }
24527
- catch (_a) {
24528
- // A consumer's rendering fault is its own problem; the pipeline
24529
- // owns real money and keeps going.
24530
- }
24769
+ class BatchSubmissionError extends Error {
24770
+ constructor(code, outcome, options) {
24771
+ super(`Batch submission ${outcome} (${code})`);
24772
+ this.name = 'BatchSubmissionError';
24773
+ this.code = code;
24774
+ this.outcome = outcome;
24775
+ // Set by hand: `Error`'s own `cause` option is ES2022.
24776
+ if (options && 'cause' in options) {
24777
+ Object.defineProperty(this, 'cause', {
24778
+ value: options.cause,
24779
+ enumerable: false,
24780
+ writable: true,
24781
+ configurable: true,
24782
+ });
24531
24783
  }
24784
+ Object.setPrototypeOf(this, BatchSubmissionError.prototype);
24532
24785
  }
24533
- /**
24534
- * Back to the initial state, abandoning any in-flight run.
24535
- *
24536
- * Subscribers are kept: the consumer that was watching the last run is the
24537
- * one that will watch the next.
24538
- */
24539
- reset() {
24540
- this._generation += 1;
24541
- this.patch(this._initial);
24542
- }
24543
- /** Claim the next run token. Any earlier run is abandoned. */
24544
- beginRun() {
24545
- this._generation += 1;
24546
- return this._generation;
24786
+ }
24787
+ /**
24788
+ * The ambiguous outcome: the batch may have landed. An instance of
24789
+ * `BatchSubmissionError` with `outcome: 'pending'`.
24790
+ */
24791
+ class BatchPendingError extends BatchSubmissionError {
24792
+ constructor(code, options) {
24793
+ super(code, 'pending', options);
24794
+ this.name = 'BatchPendingError';
24795
+ Object.setPrototypeOf(this, BatchPendingError.prototype);
24547
24796
  }
24548
24797
  }
24798
+ /** The outcome a thrown error stands for, or `null` if it is not a submission error. */
24799
+ function batchOutcomeOf(err) {
24800
+ return err instanceof BatchSubmissionError ? err.outcome : null;
24801
+ }
24802
+ /**
24803
+ * Nothing was deposited and nothing can be: a known revert, or a definitive
24804
+ * `failed` refusal. Everything else — including an error this SDK did not
24805
+ * produce — is NOT proof of that, and a consumer must not invite a retry on it.
24806
+ */
24807
+ function isDefinitivelyNotSubmitted(err) {
24808
+ return (err instanceof BatchRevertedError || batchOutcomeOf(err) === 'failed');
24809
+ }
24549
24810
 
24811
+ // ---------------------------------------------------------------------------
24812
+ // The selector tables
24813
+ // ---------------------------------------------------------------------------
24550
24814
  /**
24551
- * The run lifecycle both flows share: one run at a time, every transition
24552
- * published, and a `reset()` that is safe to call in the middle of one.
24553
- *
24554
- * `DepositFlow` and `WithdrawFlow` each carried their own copy of this — the
24555
- * re-entrancy guard, the `try/finally` around the run, `state`, `isRunning`,
24556
- * `subscribe`, `reset`. Two copies of a guard is two places for a guard to be
24557
- * wrong, on the money path, which is the same reason the pipelines themselves
24558
- * moved out of the applications.
24559
- *
24560
- * ## The guard and the run token are separate, and both matter
24815
+ * OpenZeppelin v5's ERC-20 errors. They are not in either Kasu ABI because no
24816
+ * Kasu contract declares them — they arrive from the TOKEN, through the
24817
+ * manager's `transferFrom`, and they are the modern spelling of the string
24818
+ * reverts below.
24819
+ */
24820
+ const ERC20_ERROR_ABI = [
24821
+ 'error ERC20InsufficientBalance(address sender, uint256 balance, uint256 needed)',
24822
+ 'error ERC20InsufficientAllowance(address spender, uint256 allowance, uint256 needed)',
24823
+ ];
24824
+ /**
24825
+ * The protocol errors that mean the ERC-20 transfer failed rather than the
24826
+ * protocol refusing.
24561
24827
  *
24562
- * `_runToken` is the re-entrancy guard: claimed synchronously, before the
24563
- * first `await`, so a double tap cannot launch two pipelines. The store's
24564
- * `generation` is the abandonment mechanism: an in-flight run holds the value
24565
- * it claimed, and every `patch` and `isCurrent` checkpoint it makes after
24566
- * `reset()` is dropped.
24828
+ * `SafeERC20FailedOperation` is OpenZeppelin's wrapper around a token call
24829
+ * that returned false or reverted, which on this path IS the `transferFrom`;
24830
+ * `AddressInsufficientBalance` is its native-value sibling. Both belong with
24831
+ * the balance case, not with the pool conditions — a lender who sees one of
24832
+ * them really does need to fund or re-approve.
24833
+ */
24834
+ const ERC20_FAMILY_NAMES = new Set([
24835
+ 'AddressInsufficientBalance',
24836
+ 'ERC20InsufficientAllowance',
24837
+ 'ERC20InsufficientBalance',
24838
+ 'SafeERC20FailedOperation',
24839
+ ]);
24840
+ /** Solidity's `Error(string)`, the selector every `require` reason arrives under. */
24841
+ const STRING_REVERT_SELECTOR = '0x08c379a0';
24842
+ /**
24843
+ * The `require` reasons that are a balance or an allowance, across the token
24844
+ * implementations in circulation: OpenZeppelin v4's sentences, and the
24845
+ * shorthand the minimal implementations use.
24846
+ */
24847
+ const ERC20_REASON_PATTERNS = [
24848
+ /exceeds\s+balance/,
24849
+ /exceeds\s+allowance/,
24850
+ /insufficient\s+balance/,
24851
+ /insufficient\s+allowance/,
24852
+ /transfer_from_failed/,
24853
+ /transferfrom\s+failed/,
24854
+ /low-level\s+call\s+failed/,
24855
+ ];
24856
+ /**
24857
+ * selector → error name, built once on first use.
24567
24858
  *
24568
- * `reset()` releases the guard IMMEDIATELY rather than waiting for the
24569
- * abandoned run to notice. It has to: a run parked on a wallet prompt that
24570
- * never answers is a promise that may never settle, and a consumer that has
24571
- * left that screen must be able to start a new run in the same tick. The
24572
- * abandoned run's `finally` therefore releases the guard only if it still
24573
- * holds it — by then a NEW run may own it, and clearing it there would let a
24574
- * third run start alongside the second.
24859
+ * Lazily, because a consumer that never sees a revert should not pay for three
24860
+ * `Interface` constructions at import. The two Kasu tables are the GENERATED
24861
+ * typechain ABIs, so this cannot drift from the contracts: regenerate the ABIs
24862
+ * and the new errors are recognised with no edit here.
24575
24863
  */
24576
- class Flow {
24577
- constructor(initial) {
24578
- /**
24579
- * The token of the run currently holding the guard, or `null` when none
24580
- * does. Not a boolean: releasing the guard has to be attributable to the
24581
- * run that claimed it.
24582
- */
24583
- this._runToken = null;
24584
- this._store = new FlowStore(initial);
24585
- }
24586
- /** The current state. Every transition is also published to `subscribe`. */
24587
- get state() {
24588
- return this._store.state;
24589
- }
24590
- /** True while a run is in flight, including while parked. */
24591
- get isRunning() {
24864
+ let selectorTable = null;
24865
+ function selectors() {
24866
+ if (selectorTable)
24867
+ return selectorTable;
24868
+ const table = new Map();
24869
+ const interfaces = [
24870
+ // The manager the deposit is called against, and the allow list it
24871
+ // delegates the KYC and block-window checks to.
24872
+ ILendingPoolManagerAbi__factory.createInterface(),
24873
+ IKasuAllowListAbi__factory.createInterface(),
24874
+ new ethers.ethers.utils.Interface(ERC20_ERROR_ABI),
24875
+ ];
24876
+ for (const iface of interfaces) {
24877
+ for (const fragment of Object.values(iface.errors)) {
24878
+ // First writer wins: the same name declared by both ABIs is the
24879
+ // same selector, so the duplicate would be a no-op anyway.
24880
+ const sighash = iface.getSighash(fragment);
24881
+ if (!table.has(sighash))
24882
+ table.set(sighash, fragment.name);
24883
+ }
24884
+ }
24885
+ selectorTable = table;
24886
+ return table;
24887
+ }
24888
+ // ---------------------------------------------------------------------------
24889
+ // Getting the bytes out of the envelope
24890
+ // ---------------------------------------------------------------------------
24891
+ /**
24892
+ * How deep to look for the revert data. Providers nest it differently —
24893
+ * ethers' own error wraps the provider's, which wraps the RPC body — and a
24894
+ * bound is what keeps a self-referencing error object from being walked
24895
+ * forever.
24896
+ */
24897
+ const MAX_DEPTH = 6;
24898
+ /** The keys ethers and the providers under it put a wrapped error on. */
24899
+ const NESTED_KEYS = [
24900
+ 'data',
24901
+ 'error',
24902
+ 'originalError',
24903
+ 'info',
24904
+ 'cause',
24905
+ 'body',
24906
+ ];
24907
+ /** At least a 4-byte selector, and a whole number of bytes. */
24908
+ function isRevertData(value) {
24909
+ return (value.length >= 10 &&
24910
+ value.length % 2 === 0 &&
24911
+ /^0x[0-9a-fA-F]+$/.test(value));
24912
+ }
24913
+ /**
24914
+ * Find the revert data on an error, wherever the provider left it.
24915
+ *
24916
+ * ethers v5 raises `UNPREDICTABLE_GAS_LIMIT` with the provider's own error
24917
+ * attached, and each provider nests the bytes one layer differently:
24918
+ * `error.data`, `error.error.data`, `error.data.originalError.data`, or only
24919
+ * inside the JSON `body` of a `SERVER_ERROR`. Rather than enumerate the
24920
+ * products, walk a fixed set of keys to a fixed depth and take the first value
24921
+ * that is shaped like revert data.
24922
+ */
24923
+ function extractRevertData(err) {
24924
+ return walk(err, 0);
24925
+ }
24926
+ function walk(node, depth) {
24927
+ if (node == null || depth > MAX_DEPTH)
24928
+ return null;
24929
+ if (typeof node === 'string') {
24930
+ if (isRevertData(node))
24931
+ return node;
24932
+ // A `SERVER_ERROR`'s body: the RPC's JSON reply, verbatim.
24933
+ if (!node.startsWith('{'))
24934
+ return null;
24935
+ try {
24936
+ return walk(JSON.parse(node), depth + 1);
24937
+ }
24938
+ catch (_a) {
24939
+ return null;
24940
+ }
24941
+ }
24942
+ if (typeof node !== 'object')
24943
+ return null;
24944
+ const record = node;
24945
+ for (const key of NESTED_KEYS) {
24946
+ const found = walk(record[key], depth + 1);
24947
+ if (found)
24948
+ return found;
24949
+ }
24950
+ return null;
24951
+ }
24952
+ // ---------------------------------------------------------------------------
24953
+ // Decoding
24954
+ // ---------------------------------------------------------------------------
24955
+ /**
24956
+ * Decode a failed call's revert into the error the contract named, or `null`
24957
+ * when there is nothing recognisable to name.
24958
+ *
24959
+ * `null` is the honest answer for an unknown selector, an unrelated `require`
24960
+ * string, or an error with no revert data at all — the caller keeps whatever
24961
+ * it did before rather than inventing a diagnosis from bytes it cannot read.
24962
+ */
24963
+ function decodeRevert(err) {
24964
+ const data = extractRevertData(err);
24965
+ if (!data)
24966
+ return null;
24967
+ const selector = data.slice(0, 10).toLowerCase();
24968
+ if (selector === STRING_REVERT_SELECTOR) {
24969
+ const reason = decodeStringRevert(data);
24970
+ if (reason === null)
24971
+ return null;
24972
+ return isErc20Reason(reason)
24973
+ ? { name: 'Error', family: 'erc20', reason }
24974
+ : null;
24975
+ }
24976
+ const name = selectors().get(selector);
24977
+ if (!name)
24978
+ return null;
24979
+ return {
24980
+ name,
24981
+ family: ERC20_FAMILY_NAMES.has(name) ? 'erc20' : 'protocol',
24982
+ };
24983
+ }
24984
+ function decodeStringRevert(data) {
24985
+ try {
24986
+ const [reason] = ethers.ethers.utils.defaultAbiCoder.decode(['string'], `0x${data.slice(10)}`);
24987
+ return reason;
24988
+ }
24989
+ catch (_a) {
24990
+ return null;
24991
+ }
24992
+ }
24993
+ function isErc20Reason(reason) {
24994
+ const lower = reason.toLowerCase();
24995
+ return ERC20_REASON_PATTERNS.some((pattern) => pattern.test(lower));
24996
+ }
24997
+
24998
+ /**
24999
+ * The money-path checks around an atomic, sponsored deposit batch — the
25000
+ * `approve + requestDepositWithKyc` pair `DepositFlow` hands its
25001
+ * `depositBatch` port.
25002
+ *
25003
+ * Two questions, both PURE:
25004
+ *
25005
+ * 1. Before anything is signed: is this batch exactly the deposit this run
25006
+ * asked for? (`validateDepositBatchCalls`)
25007
+ * 2. After it is mined: did THIS receipt deposit THIS amount, for THIS
25008
+ * lender, into THIS tranche? (`proveDepositInReceipt`)
25009
+ *
25010
+ * Neither knows a relay, a wallet vendor or a URL; the app's submitter calls
25011
+ * them.
25012
+ */
25013
+ /**
25014
+ * ERC-4337 EntryPoint v0.7 — the contract a bundler calls `handleOps` on. The
25015
+ * proof reads the user operation's outcome from ITS events, and from no other
25016
+ * contract's.
25017
+ */
25018
+ const ENTRY_POINT_V07 = '0x0000000071727De22E5E9d8BAf0edAc6f37da032';
25019
+ /** EntryPoint v0.7 events (eth-infinitism `IEntryPoint`). */
25020
+ const ENTRY_POINT_EVENTS = new ethers.utils.Interface([
25021
+ 'event UserOperationEvent(bytes32 indexed userOpHash, address indexed sender, address indexed paymaster, uint256 nonce, bool success, uint256 actualGasCost, uint256 actualGasUsed)',
25022
+ 'event UserOperationRevertReason(bytes32 indexed userOpHash, address indexed sender, uint256 nonce, bytes revertReason)',
25023
+ 'event BeforeExecution()',
25024
+ ]);
25025
+ /** kasu-contracts `IPendingPool` — emitted by the strategy's pending pool. */
25026
+ const PENDING_POOL_EVENTS = new ethers.utils.Interface([
25027
+ 'event DepositRequested(address indexed user, address indexed tranche, uint256 indexed dNftID, uint256 epochId, uint256 amount)',
25028
+ ]);
25029
+ const USER_OPERATION_EVENT = ENTRY_POINT_EVENTS.getEventTopic('UserOperationEvent');
25030
+ const USER_OPERATION_REVERT_REASON = ENTRY_POINT_EVENTS.getEventTopic('UserOperationRevertReason');
25031
+ const BEFORE_EXECUTION = ENTRY_POINT_EVENTS.getEventTopic('BeforeExecution');
25032
+ const DEPOSIT_REQUESTED = PENDING_POOL_EVENTS.getEventTopic('DepositRequested');
25033
+ const ERC20$1 = IERC20MetadataAbi__factory.createInterface();
25034
+ const LENDING_POOL_MANAGER$1 = ILendingPoolManagerAbi__factory.createInterface();
25035
+ const ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
25036
+ /**
25037
+ * Case-insensitive address equality; `false` for anything that is not a
25038
+ * 20-byte hex address. Checksums are not enforced — an all-lowercase address
25039
+ * is the subgraph's own spelling.
25040
+ */
25041
+ function sameAddress(a, b) {
25042
+ return (typeof a === 'string' &&
25043
+ typeof b === 'string' &&
25044
+ ADDRESS_RE.test(a) &&
25045
+ ADDRESS_RE.test(b) &&
25046
+ a.toLowerCase() === b.toLowerCase());
25047
+ }
25048
+ function isHexData(value) {
25049
+ return typeof value === 'string' && ethers.utils.isHexString(value);
25050
+ }
25051
+ /**
25052
+ * What `validateDepositBatchCalls` throws. `code` says which pin failed; the
25053
+ * message names no party, so a wallet-rejection classifier can never read it
25054
+ * as something the lender did.
25055
+ */
25056
+ class DepositBatchMismatchError extends Error {
25057
+ constructor(code) {
25058
+ super(`Deposit batch does not match the run (${code})`);
25059
+ this.name = 'DepositBatchMismatchError';
25060
+ this.code = code;
25061
+ Object.setPrototypeOf(this, DepositBatchMismatchError.prototype);
25062
+ }
25063
+ }
25064
+ function mismatch(code) {
25065
+ throw new DepositBatchMismatchError(code);
25066
+ }
25067
+ function isCallShape(call) {
25068
+ if (call === null || typeof call !== 'object')
25069
+ return false;
25070
+ const { to, data } = call;
25071
+ return typeof to === 'string' && ADDRESS_RE.test(to) && isHexData(data);
25072
+ }
25073
+ function isZeroValue(value) {
25074
+ if (value === undefined)
25075
+ return true;
25076
+ try {
25077
+ return ethers.BigNumber.from(value).isZero();
25078
+ }
25079
+ catch (_a) {
25080
+ return false;
25081
+ }
25082
+ }
25083
+ function sameAmount(a, b) {
25084
+ return ethers.BigNumber.isBigNumber(a) && a.eq(b);
25085
+ }
25086
+ /**
25087
+ * Decode `data` as a call to `name` on `iface`, and refuse it unless it is
25088
+ * the CANONICAL encoding of what it decodes to. The ABI decoder tolerates
25089
+ * trailing bytes and non-minimal offsets; a signature over bytes that decode
25090
+ * "the same" but are not the bytes the SDK produced is not a signature over
25091
+ * this run's deposit.
25092
+ */
25093
+ function decodeCanonical(iface, name, data, code) {
25094
+ let parsed;
25095
+ try {
25096
+ parsed = iface.parseTransaction({ data });
25097
+ }
25098
+ catch (_a) {
25099
+ return mismatch(code);
25100
+ }
25101
+ if (parsed.name !== name)
25102
+ return mismatch(code);
25103
+ let canonical;
25104
+ try {
25105
+ canonical = iface.encodeFunctionData(parsed.functionFragment, parsed.args);
25106
+ }
25107
+ catch (_b) {
25108
+ return mismatch(code);
25109
+ }
25110
+ if (canonical.toLowerCase() !== data.toLowerCase()) {
25111
+ return mismatch('non-canonical-encoding');
25112
+ }
25113
+ return parsed.args;
25114
+ }
25115
+ /**
25116
+ * Pin a deposit batch to the run that produced it — PURE; throws a
25117
+ * `DepositBatchMismatchError` on ANY deviation, before the lender's key signs
25118
+ * anything.
25119
+ *
25120
+ * The batch must be exactly two calls, in this order, with no native value:
25121
+ *
25122
+ * 0. `stableAsset.approve(spender, A)`;
25123
+ * 1. `spender.requestDepositWithKyc(poolId, trancheId, A, '0x', fixedTermConfigId, …)`;
25124
+ *
25125
+ * with `A` the run's own amount in BOTH — never more, never unlimited (house
25126
+ * rule: exact-amount approvals) — the run's own fixed-term config, each call
25127
+ * the canonical ABI encoding of its arguments, and no swap data. The deposit
25128
+ * data and KYC signature are not pinned here: they are signatures the
25129
+ * contract and the backend verify.
25130
+ *
25131
+ * This is the client's half; a relaying backend checks the same shape again.
25132
+ */
25133
+ function validateDepositBatchCalls(calls, expected) {
25134
+ const amount = ethers.BigNumber.from(expected.amount);
25135
+ if (amount.lte(0))
25136
+ mismatch('non-positive-amount');
25137
+ if (!Array.isArray(calls) || calls.length !== 2)
25138
+ mismatch('call-count');
25139
+ const [approveCall, depositCall] = calls;
25140
+ for (const call of [approveCall, depositCall]) {
25141
+ if (!isCallShape(call))
25142
+ mismatch('malformed-call');
25143
+ if (!isZeroValue(call.value))
25144
+ mismatch('native-value');
25145
+ }
25146
+ // 0 — the approval.
25147
+ if (!sameAddress(approveCall.to, expected.stableAsset)) {
25148
+ mismatch('approve-token');
25149
+ }
25150
+ const [approveSpender, approveAmount] = decodeCanonical(ERC20$1, 'approve', approveCall.data, 'approve-call');
25151
+ if (!sameAddress(approveSpender, expected.spender)) {
25152
+ mismatch('approve-spender');
25153
+ }
25154
+ // 1 — the deposit.
25155
+ if (!sameAddress(depositCall.to, expected.spender)) {
25156
+ mismatch('deposit-target');
25157
+ }
25158
+ const [pool, tranche, maxAmount, swapData, fixedTermConfigId] = decodeCanonical(LENDING_POOL_MANAGER$1, 'requestDepositWithKyc', depositCall.data, 'deposit-call');
25159
+ if (!sameAddress(pool, expected.poolId))
25160
+ mismatch('deposit-pool');
25161
+ if (!sameAddress(tranche, expected.trancheId))
25162
+ mismatch('deposit-tranche');
25163
+ if (swapData !== '0x')
25164
+ mismatch('swap-data');
25165
+ if (!sameAmount(fixedTermConfigId, ethers.BigNumber.from(expected.fixedTermConfigId))) {
25166
+ mismatch('fixed-term-config');
25167
+ }
25168
+ // EXACT amount, on both calls, equal to the run's.
25169
+ if (!sameAmount(approveAmount, amount))
25170
+ mismatch('approve-amount');
25171
+ if (!sameAmount(maxAmount, amount))
25172
+ mismatch('deposit-amount');
25173
+ return { tranche: tranche, amount };
25174
+ }
25175
+ /** A log's first topic, lowercased; `undefined` for a log without a topic array. */
25176
+ function topicOf(log) {
25177
+ var _a;
25178
+ return Array.isArray(log.topics)
25179
+ ? (_a = log.topics[0]) === null || _a === void 0 ? void 0 : _a.toLowerCase()
25180
+ : undefined;
25181
+ }
25182
+ function parse(iface, log) {
25183
+ try {
25184
+ return iface.parseLog({ topics: [...log.topics], data: log.data });
25185
+ }
25186
+ catch (_a) {
25187
+ return null;
25188
+ }
25189
+ }
25190
+ /**
25191
+ * Did THIS receipt deposit for THIS lender? — PURE.
25192
+ *
25193
+ * A sponsored batch lands as a bundler's `handleOps` transaction, and that
25194
+ * transaction's `status` is success even when the user operation inside it
25195
+ * reverted: the EntryPoint catches the revert, the account rolls back both
25196
+ * calls, nothing is deposited. So `status` decides NOTHING. What does:
25197
+ *
25198
+ * 1. The EntryPoint's `UserOperationEvent` for OUR operation — emitted BY
25199
+ * the EntryPoint, `sender` = lender, and `userOpHash` = the known hash
25200
+ * when there is one.
25201
+ * 2. Its `success` flag. `false` → `reverted`, naming the protocol error
25202
+ * from the matching `UserOperationRevertReason` when it decodes.
25203
+ * 3. A `DepositRequested` with `user` = lender, `tranche` = the run's and
25204
+ * `amount` = the run's, INSIDE our operation's execution: the logs after
25205
+ * the previous operation's event (or `BeforeExecution`, which precedes
25206
+ * the first) and before ours. A deposit from another operation in the
25207
+ * same bundle does not count, and neither does one emitted by a contract
25208
+ * that is not the pending pool, when the pending pool is known.
25209
+ *
25210
+ * Anything short of all three is `unproven`, which a consumer must treat as
25211
+ * PENDING — the money may have moved — and never as a failure.
25212
+ */
25213
+ function proveDepositInReceipt(receipt, expected) {
25214
+ // Typed as an array, but a receipt is foreign data: check before reading.
25215
+ if (!Array.isArray(receipt.logs)) {
25216
+ return { kind: 'unproven', reason: 'malformed-receipt' };
25217
+ }
25218
+ const amount = ethers.BigNumber.from(expected.amount);
25219
+ const op = locateUserOperation(receipt.logs, {
25220
+ sender: expected.lender,
25221
+ userOperationHash: expected.userOperationHash,
25222
+ entryPoint: expected.entryPoint,
25223
+ });
25224
+ if (op === null)
25225
+ return { kind: 'unproven', reason: 'no-user-operation' };
25226
+ if (op === 'ambiguous') {
25227
+ return { kind: 'unproven', reason: 'ambiguous-operation' };
25228
+ }
25229
+ if (!op.success) {
25230
+ return { kind: 'reverted', revertError: op.revertError() };
25231
+ }
25232
+ for (const log of op.window) {
25233
+ if (topicOf(log) !== DEPOSIT_REQUESTED)
25234
+ continue;
25235
+ if (expected.pendingPool !== undefined &&
25236
+ !sameAddress(log.address, expected.pendingPool)) {
25237
+ continue;
25238
+ }
25239
+ const decoded = parse(PENDING_POOL_EVENTS, log);
25240
+ if ((decoded === null || decoded === void 0 ? void 0 : decoded.name) !== 'DepositRequested')
25241
+ continue;
25242
+ if (!sameAddress(decoded.args.user, expected.lender))
25243
+ continue;
25244
+ if (!sameAddress(decoded.args.tranche, expected.tranche))
25245
+ continue;
25246
+ if (!sameAmount(decoded.args.amount, amount))
25247
+ continue;
25248
+ return {
25249
+ kind: 'deposited',
25250
+ dNftId: decoded.args.dNftID,
25251
+ blockNumber: receipt.blockNumber,
25252
+ };
25253
+ }
25254
+ return { kind: 'unproven', reason: 'no-deposit-requested' };
25255
+ }
25256
+ /**
25257
+ * Did THIS receipt execute the sender's user operation? — PURE. The
25258
+ * single-call sibling of `proveDepositInReceipt`, for a sponsored
25259
+ * `requestWithdrawal`, a cancel or a fixed-term withdrawal: the same
25260
+ * EntryPoint `UserOperationEvent` from the same contract, for the same
25261
+ * sender (and hash, when known), read the same way — the `handleOps`
25262
+ * transaction's own `status` decides NOTHING, because it is a success even
25263
+ * when the operation inside it reverted.
25264
+ *
25265
+ * `executed` needs the operation's `success` flag. A single-call operation
25266
+ * that succeeded executed its one call; there is no event to match beyond
25267
+ * that, so pass the `userOperationHash` whenever the submitter reports one —
25268
+ * without it, the sender's ONLY operation in the bundle is the one read, and a
25269
+ * bundle holding two of theirs is `ambiguous-operation` (pending), never the
25270
+ * first one's verdict: with one succeeded and one reverted, "executed" would
25271
+ * tell the lender their request is queued when it is not.
25272
+ */
25273
+ function proveUserOperationInReceipt(receipt, expected) {
25274
+ if (!Array.isArray(receipt.logs)) {
25275
+ return { kind: 'unproven', reason: 'malformed-receipt' };
25276
+ }
25277
+ const op = locateUserOperation(receipt.logs, expected);
25278
+ if (op === null)
25279
+ return { kind: 'unproven', reason: 'no-user-operation' };
25280
+ if (op === 'ambiguous') {
25281
+ return { kind: 'unproven', reason: 'ambiguous-operation' };
25282
+ }
25283
+ if (!op.success) {
25284
+ return { kind: 'reverted', revertError: op.revertError() };
25285
+ }
25286
+ return { kind: 'executed', blockNumber: receipt.blockNumber };
25287
+ }
25288
+ /**
25289
+ * The sender's user operation in a `handleOps` receipt — its `success` flag
25290
+ * and its execution window — `null` when the receipt does not show one, or
25291
+ * `'ambiguous'` when no hash was given and it shows more than one of theirs.
25292
+ *
25293
+ * - The `UserOperationEvent` must be emitted BY the EntryPoint, with
25294
+ * `sender` = the expected sender and, when one is known, `userOpHash` =
25295
+ * the expected hash. Without a hash the sender must have exactly ONE
25296
+ * operation in the bundle: a bundler may pack two of theirs (sequential
25297
+ * nonces, or parallel nonce keys), and reading the first would attribute
25298
+ * the other one's verdict to this call.
25299
+ * - The window is the logs after the previous operation's event (or
25300
+ * `BeforeExecution`, which precedes the first) and before ours: what our
25301
+ * operation's calls emitted, and nothing another operation in the same
25302
+ * bundle did.
25303
+ */
25304
+ function locateUserOperation(logs, expected) {
25305
+ var _a, _b, _c;
25306
+ const entryPoint = (_a = expected.entryPoint) !== null && _a !== void 0 ? _a : ENTRY_POINT_V07;
25307
+ const wantedHash = (_c = (_b = expected.userOperationHash) === null || _b === void 0 ? void 0 : _b.toLowerCase()) !== null && _c !== void 0 ? _c : null;
25308
+ const fromEntryPoint = (log) => sameAddress(log.address, entryPoint);
25309
+ let opIndex = -1;
25310
+ let success = false;
25311
+ for (let i = 0; i < logs.length; i += 1) {
25312
+ const log = logs[i];
25313
+ if (!fromEntryPoint(log) || topicOf(log) !== USER_OPERATION_EVENT) {
25314
+ continue;
25315
+ }
25316
+ const decoded = parse(ENTRY_POINT_EVENTS, log);
25317
+ if ((decoded === null || decoded === void 0 ? void 0 : decoded.name) !== 'UserOperationEvent')
25318
+ continue;
25319
+ const userOpHash = String(decoded.args.userOpHash).toLowerCase();
25320
+ if (wantedHash !== null && userOpHash !== wantedHash)
25321
+ continue;
25322
+ if (!sameAddress(decoded.args.sender, expected.sender))
25323
+ continue;
25324
+ // A second operation of this sender, and no hash to tell them apart.
25325
+ if (opIndex >= 0)
25326
+ return 'ambiguous';
25327
+ opIndex = i;
25328
+ success = decoded.args.success === true;
25329
+ // A hash names exactly one operation; without one, keep scanning so
25330
+ // a second operation of the sender is noticed.
25331
+ if (wantedHash !== null)
25332
+ break;
25333
+ }
25334
+ if (opIndex < 0)
25335
+ return null;
25336
+ // Our operation's execution window.
25337
+ let windowStart = 0;
25338
+ for (let i = opIndex - 1; i >= 0; i -= 1) {
25339
+ const log = logs[i];
25340
+ const topic = topicOf(log);
25341
+ if (fromEntryPoint(log) &&
25342
+ (topic === USER_OPERATION_EVENT || topic === BEFORE_EXECUTION)) {
25343
+ windowStart = i + 1;
25344
+ break;
25345
+ }
25346
+ }
25347
+ const window = logs.slice(windowStart, opIndex);
25348
+ return {
25349
+ success,
25350
+ window,
25351
+ revertError: () => revertErrorIn(window, expected.sender, fromEntryPoint),
25352
+ };
25353
+ }
25354
+ /**
25355
+ * The protocol error a reverted operation named, or `''`. Only a PROTOCOL
25356
+ * error is passed on: an `Error(string)` or an ERC-20 failure is not a name a
25357
+ * consumer has words for.
25358
+ */
25359
+ function revertErrorIn(window, lender, fromEntryPoint) {
25360
+ for (const log of window) {
25361
+ if (!fromEntryPoint(log))
25362
+ continue;
25363
+ // Through the same guard as the main loop: a malformed log beside the
25364
+ // revert reason must not turn a DEFINITIVE revert into "pending".
25365
+ if (topicOf(log) !== USER_OPERATION_REVERT_REASON)
25366
+ continue;
25367
+ const decoded = parse(ENTRY_POINT_EVENTS, log);
25368
+ if ((decoded === null || decoded === void 0 ? void 0 : decoded.name) !== 'UserOperationRevertReason')
25369
+ continue;
25370
+ if (!sameAddress(decoded.args.sender, lender))
25371
+ continue;
25372
+ const revert = decodeRevert(decoded.args.revertReason);
25373
+ return (revert === null || revert === void 0 ? void 0 : revert.family) === 'protocol' ? revert.name : '';
25374
+ }
25375
+ return '';
25376
+ }
25377
+
25378
+ /**
25379
+ * First read at once, then 3 s backing off ×1.5 to a 10 s ceiling, for up to
25380
+ * 10 minutes. Running out of time is PENDING: the batch was accepted and may
25381
+ * still land.
25382
+ */
25383
+ const DEFAULT_BATCH_POLLING_POLICY = Object.freeze({
25384
+ initialMs: 3000,
25385
+ maxMs: 10000,
25386
+ backoff: 1.5,
25387
+ timeoutMs: 10 * 60 * 1000,
25388
+ });
25389
+ function resolvePolicy(policy) {
25390
+ var _a, _b, _c, _d;
25391
+ const resolved = {
25392
+ initialMs: (_a = policy === null || policy === void 0 ? void 0 : policy.initialMs) !== null && _a !== void 0 ? _a : DEFAULT_BATCH_POLLING_POLICY.initialMs,
25393
+ maxMs: (_b = policy === null || policy === void 0 ? void 0 : policy.maxMs) !== null && _b !== void 0 ? _b : DEFAULT_BATCH_POLLING_POLICY.maxMs,
25394
+ backoff: (_c = policy === null || policy === void 0 ? void 0 : policy.backoff) !== null && _c !== void 0 ? _c : DEFAULT_BATCH_POLLING_POLICY.backoff,
25395
+ timeoutMs: (_d = policy === null || policy === void 0 ? void 0 : policy.timeoutMs) !== null && _d !== void 0 ? _d : DEFAULT_BATCH_POLLING_POLICY.timeoutMs,
25396
+ };
25397
+ const { initialMs, maxMs, backoff, timeoutMs } = resolved;
25398
+ if (!(Number.isFinite(initialMs) && initialMs > 0) ||
25399
+ !(Number.isFinite(maxMs) && maxMs >= initialMs) ||
25400
+ !(Number.isFinite(backoff) && backoff >= 1) ||
25401
+ !(Number.isFinite(timeoutMs) && timeoutMs > 0)) {
25402
+ throw new TypeError('submitBatch: invalid polling policy');
25403
+ }
25404
+ return resolved;
25405
+ }
25406
+ /**
25407
+ * Submit `calls` and return once the submitter ACCEPTED them; `wait()` then
25408
+ * polls and proves. Use it as the body of a `DepositFlow` `depositBatch`
25409
+ * port, after `validateDepositBatchCalls` and the app's own signing:
25410
+ *
25411
+ * ```ts
25412
+ * depositBatch: (calls) => {
25413
+ * const { tranche, amount } = validateDepositBatchCalls(calls, expected);
25414
+ * return submitBatch(
25415
+ * // `pendingPool`: the strategy's pending pool, so only ITS
25416
+ * // `DepositRequested` can prove the deposit.
25417
+ * { calls, expect: { lender, tranche, amount, pendingPool } },
25418
+ * { post, getStatus, getReceipt, now: Date.now, sleep },
25419
+ * );
25420
+ * },
25421
+ * ```
25422
+ *
25423
+ * | what happened | outcome |
25424
+ * | ---------------------------------------------------- | ---------------------------------------- |
25425
+ * | `post` throws `BatchRevertedError` | rethrown — reverted, nothing deposited |
25426
+ * | `post` throws `BatchSubmissionError` | rethrown with its own outcome |
25427
+ * | `post` throws anything else | `BatchPendingError('post_unknown')` |
25428
+ * | `post` resolves without a transaction id | `BatchPendingError('malformed_answer')` |
25429
+ * | status `reverted` | `BatchRevertedError('')` |
25430
+ * | status `unknown` | `BatchPendingError(code)` |
25431
+ * | polling runs out of time | `BatchPendingError('status_timeout')` |
25432
+ * | receipt unreadable | `BatchPendingError('receipt_unavailable')` |
25433
+ * | receipt: user operation reverted | `BatchRevertedError(revertError)` |
25434
+ * | receipt: this deposit not shown | `BatchPendingError('deposit_unproven:<reason>')` |
25435
+ * | receipt: this lender's deposit, tranche and amount | resolves `ProvenBatch` |
25436
+ *
25437
+ * Nothing on this path ever produces a `failed` outcome: by the time a batch
25438
+ * has been posted, only a revert proves that nothing was deposited.
25439
+ */
25440
+ function submitBatch(input, ports, policy) {
25441
+ return __awaiter(this, void 0, void 0, function* () {
25442
+ const resolved = resolvePolicy(policy);
25443
+ const transactionId = yield postOnce(() => ports.post(input.calls));
25444
+ let settled = null;
25445
+ return {
25446
+ transactionId,
25447
+ // Memoised: a second `wait()` reads the same outcome instead of
25448
+ // polling the submitter twice.
25449
+ wait: () => (settled !== null && settled !== void 0 ? settled : (settled = settle(transactionId, input.expect, ports, resolved))),
25450
+ };
25451
+ });
25452
+ }
25453
+ /**
25454
+ * The post, and what its answer means: the transaction id, or the error the
25455
+ * outcome table names. Shared by both submitters, so the two cannot come to
25456
+ * disagree about which throws are ambiguous.
25457
+ */
25458
+ function postOnce(post) {
25459
+ return __awaiter(this, void 0, void 0, function* () {
25460
+ let accepted;
25461
+ try {
25462
+ accepted = yield post();
25463
+ }
25464
+ catch (err) {
25465
+ if (err instanceof BatchRevertedError)
25466
+ throw err;
25467
+ if (err instanceof BatchSubmissionError)
25468
+ throw err;
25469
+ throw new BatchPendingError('post_unknown', { cause: err });
25470
+ }
25471
+ const transactionId = accepted === null || accepted === void 0 ? void 0 : accepted.transactionId;
25472
+ if (typeof transactionId !== 'string' || transactionId === '') {
25473
+ throw new BatchPendingError('malformed_answer');
25474
+ }
25475
+ return transactionId;
25476
+ });
25477
+ }
25478
+ function readStatus(ports, transactionId) {
25479
+ return __awaiter(this, void 0, void 0, function* () {
25480
+ try {
25481
+ return yield ports.getStatus(transactionId);
25482
+ }
25483
+ catch (_a) {
25484
+ return { kind: 'again' };
25485
+ }
25486
+ });
25487
+ }
25488
+ /** Poll until a mined hash, or a typed reason there will not be one. */
25489
+ function waitForMined(transactionId, ports, policy) {
25490
+ return __awaiter(this, void 0, void 0, function* () {
25491
+ var _a;
25492
+ const deadline = ports.now() + policy.timeoutMs;
25493
+ let userOperationHash = null;
25494
+ let delay = policy.initialMs;
25495
+ for (;;) {
25496
+ const read = yield readStatus(ports, transactionId);
25497
+ switch (read.kind) {
25498
+ case 'mined':
25499
+ if (typeof read.txHash !== 'string' || read.txHash === '') {
25500
+ break; // a malformed "mined" is not an answer; ask again
25501
+ }
25502
+ return {
25503
+ txHash: read.txHash,
25504
+ userOperationHash: (_a = read.userOperationHash) !== null && _a !== void 0 ? _a : userOperationHash,
25505
+ };
25506
+ case 'reverted':
25507
+ throw new BatchRevertedError('');
25508
+ case 'unknown':
25509
+ throw new BatchPendingError(read.code);
25510
+ case 'again':
25511
+ if (read.userOperationHash) {
25512
+ userOperationHash = read.userOperationHash;
25513
+ }
25514
+ break;
25515
+ }
25516
+ if (ports.now() + delay > deadline) {
25517
+ throw new BatchPendingError('status_timeout');
25518
+ }
25519
+ yield ports.sleep(delay);
25520
+ delay = Math.min(policy.maxMs, Math.round(delay * policy.backoff));
25521
+ }
25522
+ });
25523
+ }
25524
+ /** Poll to a mined hash and read its receipt; a receipt read that throws is PENDING. */
25525
+ function minedReceipt(transactionId, ports, policy) {
25526
+ return __awaiter(this, void 0, void 0, function* () {
25527
+ const mined = yield waitForMined(transactionId, ports, policy);
25528
+ try {
25529
+ return {
25530
+ receipt: yield ports.getReceipt(mined.txHash),
25531
+ txHash: mined.txHash,
25532
+ userOperationHash: mined.userOperationHash,
25533
+ };
25534
+ }
25535
+ catch (err) {
25536
+ throw new BatchPendingError('receipt_unavailable', { cause: err });
25537
+ }
25538
+ });
25539
+ }
25540
+ function settle(transactionId, expect, ports, policy) {
25541
+ return __awaiter(this, void 0, void 0, function* () {
25542
+ const mined = yield minedReceipt(transactionId, ports, policy);
25543
+ let proof;
25544
+ try {
25545
+ proof = proveDepositInReceipt(mined.receipt, Object.assign(Object.assign({}, expect), { userOperationHash: mined.userOperationHash }));
25546
+ }
25547
+ catch (err) {
25548
+ // A receipt the proof cannot even read proves nothing either way.
25549
+ throw new BatchPendingError('deposit_unproven', { cause: err });
25550
+ }
25551
+ if (proof.kind === 'reverted')
25552
+ throw new BatchRevertedError(proof.revertError);
25553
+ if (proof.kind === 'unproven') {
25554
+ // The proof's reason rides in the code: "no-user-operation" (the hash
25555
+ // is not this lender's operation at all) and "no-deposit-requested"
25556
+ // (the operation ran and shows no deposit) are the two things an
25557
+ // operator reading the alert needs to tell apart first.
25558
+ throw new BatchPendingError(`deposit_unproven:${proof.reason}`);
25559
+ }
25560
+ return { proof, receipt: mined.receipt, txHash: mined.txHash };
25561
+ });
25562
+ }
25563
+ /**
25564
+ * What became of an earlier submission that ended PENDING — ONE status read,
25565
+ * no polling — for an app's guard against lending twice:
25566
+ *
25567
+ * - `deposited` — mined, and the receipt proves the deposit;
25568
+ * - `not-deposited` — definitively reverted (vendor status or the user
25569
+ * operation's own flag);
25570
+ * - `unknown` — anything else, including a read or receipt that failed.
25571
+ *
25572
+ * Only `not-deposited` may let a new lend proceed unprompted; `deposited`
25573
+ * means the lender is told the earlier one landed (`already-submitted`).
25574
+ */
25575
+ function checkBatchOutcome(transactionId, expect, ports) {
25576
+ return __awaiter(this, void 0, void 0, function* () {
25577
+ var _a;
25578
+ let read;
25579
+ try {
25580
+ read = yield ports.getStatus(transactionId);
25581
+ }
25582
+ catch (_b) {
25583
+ return 'unknown';
25584
+ }
25585
+ if (read.kind === 'reverted')
25586
+ return 'not-deposited';
25587
+ if (read.kind !== 'mined' || typeof read.txHash !== 'string') {
25588
+ return 'unknown';
25589
+ }
25590
+ try {
25591
+ const receipt = yield ports.getReceipt(read.txHash);
25592
+ const proof = proveDepositInReceipt(receipt, Object.assign(Object.assign({}, expect), { userOperationHash: (_a = read.userOperationHash) !== null && _a !== void 0 ? _a : null }));
25593
+ if (proof.kind === 'deposited')
25594
+ return 'deposited';
25595
+ if (proof.kind === 'reverted')
25596
+ return 'not-deposited';
25597
+ return 'unknown';
25598
+ }
25599
+ catch (_c) {
25600
+ return 'unknown';
25601
+ }
25602
+ });
25603
+ }
25604
+ /**
25605
+ * `submitBatch` for ONE sponsored `LendingPoolManager` call — the body of a
25606
+ * `WithdrawFlow` `submitCall` port, and of an app's cancel or
25607
+ * withdraw-at-expiry action. Same post, same polling policy, same outcome
25608
+ * table; the proof is `proveUserOperationInReceipt` (the sender's operation
25609
+ * succeeded) instead of the deposit proof, and an unproven receipt is
25610
+ * `call_unproven:<reason>`.
25611
+ *
25612
+ * ```ts
25613
+ * submitCall: (call) =>
25614
+ * submitSponsoredCall(
25615
+ * { call, expect: { sender: lender } },
25616
+ * { post: (c) => postToMyRelay(c.kind, [c.call]), getStatus, getReceipt, now, sleep },
25617
+ * ),
25618
+ * ```
25619
+ *
25620
+ * The call is checked against its kind before anything is posted — a known
25621
+ * kind, exactly `{ to, data }`, and `data` opening with that kind's selector —
25622
+ * and a mismatch is `BatchSubmissionError('call_mismatch', 'failed')`: nothing
25623
+ * was sent. Build calls with `kasu.calls`, and this never fires.
25624
+ *
25625
+ * As on the deposit path, nothing after the post ever produces `failed`.
25626
+ */
25627
+ function submitSponsoredCall(input, ports, policy) {
25628
+ return __awaiter(this, void 0, void 0, function* () {
25629
+ const resolved = resolvePolicy(policy);
25630
+ if (!isWellFormedSponsoredCall(input.call)) {
25631
+ throw new BatchSubmissionError('call_mismatch', 'failed');
25632
+ }
25633
+ const transactionId = yield postOnce(() => ports.post(input.call));
25634
+ let settled = null;
25635
+ return {
25636
+ transactionId,
25637
+ wait: () => (settled !== null && settled !== void 0 ? settled : (settled = settleCall(transactionId, input.expect, ports, resolved))),
25638
+ };
25639
+ });
25640
+ }
25641
+ function isWellFormedSponsoredCall(value) {
25642
+ if (value === null || typeof value !== 'object')
25643
+ return false;
25644
+ const { kind, call } = value;
25645
+ if (typeof kind !== 'string' ||
25646
+ !SPONSORED_CALL_KINDS.includes(kind)) {
25647
+ return false;
25648
+ }
25649
+ if (call === null || typeof call !== 'object')
25650
+ return false;
25651
+ const keys = Object.keys(call);
25652
+ if (keys.length !== 2 || !keys.includes('to') || !keys.includes('data')) {
25653
+ return false;
25654
+ }
25655
+ const { to, data } = call;
25656
+ return (typeof to === 'string' &&
25657
+ /^0x[0-9a-fA-F]{40}$/.test(to) &&
25658
+ typeof data === 'string' &&
25659
+ /^0x(?:[0-9a-fA-F]{2})*$/.test(data) &&
25660
+ data
25661
+ .toLowerCase()
25662
+ .startsWith(SPONSORED_CALL_SELECTORS[kind]));
25663
+ }
25664
+ function settleCall(transactionId, expect, ports, policy) {
25665
+ return __awaiter(this, void 0, void 0, function* () {
25666
+ const mined = yield minedReceipt(transactionId, ports, policy);
25667
+ let proof;
25668
+ try {
25669
+ proof = proveUserOperationInReceipt(mined.receipt, Object.assign(Object.assign({}, expect), { userOperationHash: mined.userOperationHash }));
25670
+ }
25671
+ catch (err) {
25672
+ throw new BatchPendingError('call_unproven', { cause: err });
25673
+ }
25674
+ if (proof.kind === 'reverted') {
25675
+ throw new BatchRevertedError(proof.revertError);
25676
+ }
25677
+ if (proof.kind === 'unproven') {
25678
+ throw new BatchPendingError(`call_unproven:${proof.reason}`);
25679
+ }
25680
+ return { proof, receipt: mined.receipt, txHash: mined.txHash };
25681
+ });
25682
+ }
25683
+
25684
+ /**
25685
+ * The tiny observable-state kernel both flows are built on.
25686
+ *
25687
+ * Deliberately not a framework store: no React, no signals, no dependency.
25688
+ * A flow is a long asynchronous run that has to publish every transition to
25689
+ * whatever is rendering it, and has to be able to abandon a run that the
25690
+ * consumer walked away from. That is the whole requirement, and it is 60 lines.
25691
+ *
25692
+ * The `generation` counter is the abandonment mechanism. `reset()` bumps it;
25693
+ * an in-flight run captured the previous value, so every later `patch` it
25694
+ * attempts is dropped and every `isCurrent` checkpoint tells it to stop. It is
25695
+ * what makes `reset()` safe to call mid-flight — the run cannot resurrect a
25696
+ * dead state or, worse, drive the UI back into a phase the consumer has left.
25697
+ */
25698
+ class FlowStore {
25699
+ constructor(_initial) {
25700
+ this._initial = _initial;
25701
+ this._generation = 0;
25702
+ this._listeners = new Set();
25703
+ this._state = _initial;
25704
+ }
25705
+ get state() {
25706
+ return this._state;
25707
+ }
25708
+ /** The run token an in-flight pipeline captures at `start()`. */
25709
+ get generation() {
25710
+ return this._generation;
25711
+ }
25712
+ /** False once `reset()` (or a later `start()`) has superseded `token`. */
25713
+ isCurrent(token) {
25714
+ return this._generation === token;
25715
+ }
25716
+ /**
25717
+ * Subscribe to every transition. Returns the unsubscribe function; calling
25718
+ * it twice is harmless.
25719
+ *
25720
+ * Listeners are NOT called on subscribe — read `state` for the current
25721
+ * value. A listener that throws is not allowed to break the pipeline or
25722
+ * starve the listeners registered after it, so throws are swallowed.
25723
+ */
25724
+ subscribe(listener) {
25725
+ this._listeners.add(listener);
25726
+ return () => {
25727
+ this._listeners.delete(listener);
25728
+ };
25729
+ }
25730
+ /**
25731
+ * Merge `next` into the state and notify, unless `token` names a
25732
+ * superseded run.
25733
+ */
25734
+ patch(next, token) {
25735
+ if (token !== undefined && !this.isCurrent(token))
25736
+ return;
25737
+ this._state = Object.assign(Object.assign({}, this._state), next);
25738
+ for (const listener of [...this._listeners]) {
25739
+ try {
25740
+ listener(this._state);
25741
+ }
25742
+ catch (_a) {
25743
+ // A consumer's rendering fault is its own problem; the pipeline
25744
+ // owns real money and keeps going.
25745
+ }
25746
+ }
25747
+ }
25748
+ /**
25749
+ * Back to the initial state, abandoning any in-flight run.
25750
+ *
25751
+ * Subscribers are kept: the consumer that was watching the last run is the
25752
+ * one that will watch the next.
25753
+ */
25754
+ reset() {
25755
+ this._generation += 1;
25756
+ this.patch(this._initial);
25757
+ }
25758
+ /** Claim the next run token. Any earlier run is abandoned. */
25759
+ beginRun() {
25760
+ this._generation += 1;
25761
+ return this._generation;
25762
+ }
25763
+ }
25764
+
25765
+ /**
25766
+ * The run lifecycle both flows share: one run at a time, every transition
25767
+ * published, and a `reset()` that is safe to call in the middle of one.
25768
+ *
25769
+ * `DepositFlow` and `WithdrawFlow` each carried their own copy of this — the
25770
+ * re-entrancy guard, the `try/finally` around the run, `state`, `isRunning`,
25771
+ * `subscribe`, `reset`. Two copies of a guard is two places for a guard to be
25772
+ * wrong, on the money path, which is the same reason the pipelines themselves
25773
+ * moved out of the applications.
25774
+ *
25775
+ * ## The guard and the run token are separate, and both matter
25776
+ *
25777
+ * `_runToken` is the re-entrancy guard: claimed synchronously, before the
25778
+ * first `await`, so a double tap cannot launch two pipelines. The store's
25779
+ * `generation` is the abandonment mechanism: an in-flight run holds the value
25780
+ * it claimed, and every `patch` and `isCurrent` checkpoint it makes after
25781
+ * `reset()` is dropped.
25782
+ *
25783
+ * `reset()` releases the guard IMMEDIATELY rather than waiting for the
25784
+ * abandoned run to notice. It has to: a run parked on a wallet prompt that
25785
+ * never answers is a promise that may never settle, and a consumer that has
25786
+ * left that screen must be able to start a new run in the same tick. The
25787
+ * abandoned run's `finally` therefore releases the guard only if it still
25788
+ * holds it — by then a NEW run may own it, and clearing it there would let a
25789
+ * third run start alongside the second.
25790
+ */
25791
+ class Flow {
25792
+ constructor(initial) {
25793
+ /**
25794
+ * The token of the run currently holding the guard, or `null` when none
25795
+ * does. Not a boolean: releasing the guard has to be attributable to the
25796
+ * run that claimed it.
25797
+ */
25798
+ this._runToken = null;
25799
+ this._store = new FlowStore(initial);
25800
+ }
25801
+ /** The current state. Every transition is also published to `subscribe`. */
25802
+ get state() {
25803
+ return this._store.state;
25804
+ }
25805
+ /** True while a run is in flight, including while parked. */
25806
+ get isRunning() {
24592
25807
  return this._runToken !== null;
24593
25808
  }
24594
25809
  /**
@@ -24806,193 +26021,6 @@ function buildFullNameRequestMessage(address, timestampMs) {
24806
26021
  return `I request my full name for ${address.toLowerCase()} at ${timestampMs}.`;
24807
26022
  }
24808
26023
 
24809
- // ---------------------------------------------------------------------------
24810
- // The selector tables
24811
- // ---------------------------------------------------------------------------
24812
- /**
24813
- * OpenZeppelin v5's ERC-20 errors. They are not in either Kasu ABI because no
24814
- * Kasu contract declares them — they arrive from the TOKEN, through the
24815
- * manager's `transferFrom`, and they are the modern spelling of the string
24816
- * reverts below.
24817
- */
24818
- const ERC20_ERROR_ABI = [
24819
- 'error ERC20InsufficientBalance(address sender, uint256 balance, uint256 needed)',
24820
- 'error ERC20InsufficientAllowance(address spender, uint256 allowance, uint256 needed)',
24821
- ];
24822
- /**
24823
- * The protocol errors that mean the ERC-20 transfer failed rather than the
24824
- * protocol refusing.
24825
- *
24826
- * `SafeERC20FailedOperation` is OpenZeppelin's wrapper around a token call
24827
- * that returned false or reverted, which on this path IS the `transferFrom`;
24828
- * `AddressInsufficientBalance` is its native-value sibling. Both belong with
24829
- * the balance case, not with the pool conditions — a lender who sees one of
24830
- * them really does need to fund or re-approve.
24831
- */
24832
- const ERC20_FAMILY_NAMES = new Set([
24833
- 'AddressInsufficientBalance',
24834
- 'ERC20InsufficientAllowance',
24835
- 'ERC20InsufficientBalance',
24836
- 'SafeERC20FailedOperation',
24837
- ]);
24838
- /** Solidity's `Error(string)`, the selector every `require` reason arrives under. */
24839
- const STRING_REVERT_SELECTOR = '0x08c379a0';
24840
- /**
24841
- * The `require` reasons that are a balance or an allowance, across the token
24842
- * implementations in circulation: OpenZeppelin v4's sentences, and the
24843
- * shorthand the minimal implementations use.
24844
- */
24845
- const ERC20_REASON_PATTERNS = [
24846
- /exceeds\s+balance/,
24847
- /exceeds\s+allowance/,
24848
- /insufficient\s+balance/,
24849
- /insufficient\s+allowance/,
24850
- /transfer_from_failed/,
24851
- /transferfrom\s+failed/,
24852
- /low-level\s+call\s+failed/,
24853
- ];
24854
- /**
24855
- * selector → error name, built once on first use.
24856
- *
24857
- * Lazily, because a consumer that never sees a revert should not pay for three
24858
- * `Interface` constructions at import. The two Kasu tables are the GENERATED
24859
- * typechain ABIs, so this cannot drift from the contracts: regenerate the ABIs
24860
- * and the new errors are recognised with no edit here.
24861
- */
24862
- let selectorTable = null;
24863
- function selectors() {
24864
- if (selectorTable)
24865
- return selectorTable;
24866
- const table = new Map();
24867
- const interfaces = [
24868
- // The manager the deposit is called against, and the allow list it
24869
- // delegates the KYC and block-window checks to.
24870
- ILendingPoolManagerAbi__factory.createInterface(),
24871
- IKasuAllowListAbi__factory.createInterface(),
24872
- new ethers.ethers.utils.Interface(ERC20_ERROR_ABI),
24873
- ];
24874
- for (const iface of interfaces) {
24875
- for (const fragment of Object.values(iface.errors)) {
24876
- // First writer wins: the same name declared by both ABIs is the
24877
- // same selector, so the duplicate would be a no-op anyway.
24878
- const sighash = iface.getSighash(fragment);
24879
- if (!table.has(sighash))
24880
- table.set(sighash, fragment.name);
24881
- }
24882
- }
24883
- selectorTable = table;
24884
- return table;
24885
- }
24886
- // ---------------------------------------------------------------------------
24887
- // Getting the bytes out of the envelope
24888
- // ---------------------------------------------------------------------------
24889
- /**
24890
- * How deep to look for the revert data. Providers nest it differently —
24891
- * ethers' own error wraps the provider's, which wraps the RPC body — and a
24892
- * bound is what keeps a self-referencing error object from being walked
24893
- * forever.
24894
- */
24895
- const MAX_DEPTH = 6;
24896
- /** The keys ethers and the providers under it put a wrapped error on. */
24897
- const NESTED_KEYS = [
24898
- 'data',
24899
- 'error',
24900
- 'originalError',
24901
- 'info',
24902
- 'cause',
24903
- 'body',
24904
- ];
24905
- /** At least a 4-byte selector, and a whole number of bytes. */
24906
- function isRevertData(value) {
24907
- return (value.length >= 10 &&
24908
- value.length % 2 === 0 &&
24909
- /^0x[0-9a-fA-F]+$/.test(value));
24910
- }
24911
- /**
24912
- * Find the revert data on an error, wherever the provider left it.
24913
- *
24914
- * ethers v5 raises `UNPREDICTABLE_GAS_LIMIT` with the provider's own error
24915
- * attached, and each provider nests the bytes one layer differently:
24916
- * `error.data`, `error.error.data`, `error.data.originalError.data`, or only
24917
- * inside the JSON `body` of a `SERVER_ERROR`. Rather than enumerate the
24918
- * products, walk a fixed set of keys to a fixed depth and take the first value
24919
- * that is shaped like revert data.
24920
- */
24921
- function extractRevertData(err) {
24922
- return walk(err, 0);
24923
- }
24924
- function walk(node, depth) {
24925
- if (node == null || depth > MAX_DEPTH)
24926
- return null;
24927
- if (typeof node === 'string') {
24928
- if (isRevertData(node))
24929
- return node;
24930
- // A `SERVER_ERROR`'s body: the RPC's JSON reply, verbatim.
24931
- if (!node.startsWith('{'))
24932
- return null;
24933
- try {
24934
- return walk(JSON.parse(node), depth + 1);
24935
- }
24936
- catch (_a) {
24937
- return null;
24938
- }
24939
- }
24940
- if (typeof node !== 'object')
24941
- return null;
24942
- const record = node;
24943
- for (const key of NESTED_KEYS) {
24944
- const found = walk(record[key], depth + 1);
24945
- if (found)
24946
- return found;
24947
- }
24948
- return null;
24949
- }
24950
- // ---------------------------------------------------------------------------
24951
- // Decoding
24952
- // ---------------------------------------------------------------------------
24953
- /**
24954
- * Decode a failed call's revert into the error the contract named, or `null`
24955
- * when there is nothing recognisable to name.
24956
- *
24957
- * `null` is the honest answer for an unknown selector, an unrelated `require`
24958
- * string, or an error with no revert data at all — the caller keeps whatever
24959
- * it did before rather than inventing a diagnosis from bytes it cannot read.
24960
- */
24961
- function decodeRevert(err) {
24962
- const data = extractRevertData(err);
24963
- if (!data)
24964
- return null;
24965
- const selector = data.slice(0, 10).toLowerCase();
24966
- if (selector === STRING_REVERT_SELECTOR) {
24967
- const reason = decodeStringRevert(data);
24968
- if (reason === null)
24969
- return null;
24970
- return isErc20Reason(reason)
24971
- ? { name: 'Error', family: 'erc20', reason }
24972
- : null;
24973
- }
24974
- const name = selectors().get(selector);
24975
- if (!name)
24976
- return null;
24977
- return {
24978
- name,
24979
- family: ERC20_FAMILY_NAMES.has(name) ? 'erc20' : 'protocol',
24980
- };
24981
- }
24982
- function decodeStringRevert(data) {
24983
- try {
24984
- const [reason] = ethers.ethers.utils.defaultAbiCoder.decode(['string'], `0x${data.slice(10)}`);
24985
- return reason;
24986
- }
24987
- catch (_a) {
24988
- return null;
24989
- }
24990
- }
24991
- function isErc20Reason(reason) {
24992
- const lower = reason.toLowerCase();
24993
- return ERC20_REASON_PATTERNS.some((pattern) => pattern.test(lower));
24994
- }
24995
-
24996
26024
  /**
24997
26025
  * Wallet and RPC error predicates — pure functions over `unknown`.
24998
26026
  *
@@ -25174,9 +26202,43 @@ const INITIAL$1 = {
25174
26202
  stepIndex: 0,
25175
26203
  stepTotal: 4,
25176
26204
  approvalRequired: true,
26205
+ batched: false,
25177
26206
  contract: null,
25178
26207
  failure: null,
25179
26208
  };
26209
+ const ERC20 = IERC20MetadataAbi__factory.createInterface();
26210
+ const LENDING_POOL_MANAGER = ILendingPoolManagerAbi__factory.createInterface();
26211
+ /**
26212
+ * The batch path's two calls, encoded by the SDK so no consumer has to: the
26213
+ * EXACT-amount approve, then the very `requestDepositWithKyc` the default
26214
+ * `deposit` port sends — `swapData` `'0x'`, no value. Both read `amount` from
26215
+ * the same variable, so the batch cannot approve one figure and deposit
26216
+ * another.
26217
+ */
26218
+ function encodeDepositBatch(args) {
26219
+ const { stableAsset, spender, input, depositData, kyc } = args;
26220
+ return [
26221
+ {
26222
+ to: stableAsset,
26223
+ data: ERC20.encodeFunctionData('approve', [spender, input.amount]),
26224
+ },
26225
+ {
26226
+ to: spender,
26227
+ data: LENDING_POOL_MANAGER.encodeFunctionData('requestDepositWithKyc', [
26228
+ input.poolId,
26229
+ input.trancheId,
26230
+ input.amount,
26231
+ '0x',
26232
+ input.fixedTermConfigId,
26233
+ depositData,
26234
+ {
26235
+ blockExpiration: kyc.blockExpiration,
26236
+ signature: kyc.signature,
26237
+ },
26238
+ ]),
26239
+ },
26240
+ ];
26241
+ }
25180
26242
  /** 1-based badge position, with `approve` dropped when it is out of scope. */
25181
26243
  function stepIndexOf(step, approvalRequired) {
25182
26244
  const order = approvalRequired
@@ -25220,7 +26282,33 @@ function classifyRequest(err) {
25220
26282
  if (revert !== null || isUnpredictableGas(err)) {
25221
26283
  return { step: 'request', reason: 'insufficient-balance', error: err };
25222
26284
  }
25223
- return classifyWalletFailure('request', err);
26285
+ return classifyWalletFailure('request', err);
26286
+ }
26287
+ /**
26288
+ * A `depositBatch` rejection. A submitter that KNOWS the batch reverts says so
26289
+ * with `BatchRevertedError`; anything else is read exactly as a direct
26290
+ * deposit's throw is, so a non-wallet error can never become `cancelled`.
26291
+ */
26292
+ function classifyBatch(err) {
26293
+ // A submission outcome is read FIRST and taken at its word. It must never
26294
+ // reach the revert-data walk below: that walk follows `cause`, and an
26295
+ // ambiguous outcome whose cause happens to carry revert bytes would read
26296
+ // as `reverted` or `insufficient-balance` — "nothing was deposited, try
26297
+ // again" — on a batch that may have moved the money.
26298
+ if (err instanceof BatchSubmissionError) {
26299
+ return err.outcome === 'failed'
26300
+ ? { step: 'request', reason: 'failed', error: err }
26301
+ : { step: 'request', reason: err.outcome, error: err };
26302
+ }
26303
+ if (err instanceof BatchRevertedError) {
26304
+ return {
26305
+ step: 'request',
26306
+ reason: 'reverted',
26307
+ revertError: err.revertError,
26308
+ error: err,
26309
+ };
26310
+ }
26311
+ return classifyRequest(err);
25224
26312
  }
25225
26313
  // ---------------------------------------------------------------------------
25226
26314
  // The flow
@@ -25248,8 +26336,15 @@ class DepositFlow extends Flow {
25248
26336
  * A stale token simply is not the current generation.
25249
26337
  */
25250
26338
  this._acceptingFor = null;
26339
+ /**
26340
+ * The batch path's authorisation handshake. Same shape and lifetime as
26341
+ * `_accept`: the run parks on it, `authoriseTransfer`, `declineTransfer`
26342
+ * and `reset` settle it, and it is cleared the moment it settles.
26343
+ */
26344
+ this._authorise = null;
25251
26345
  this._ttlMs = (_a = opts === null || opts === void 0 ? void 0 : opts.contractTtlMs) !== null && _a !== void 0 ? _a : CONTRACT_TTL_MS;
25252
26346
  this._defaultSpender = opts === null || opts === void 0 ? void 0 : opts.spender;
26347
+ this._stableAsset = opts === null || opts === void 0 ? void 0 : opts.stableAsset;
25253
26348
  // Called through the ports object, never captured off it: a consumer
25254
26349
  // whose clock is a method on its own object keeps its `this`.
25255
26350
  this._now = () => { var _a, _b; return (_b = (_a = _ports.now) === null || _a === void 0 ? void 0 : _a.call(_ports)) !== null && _b !== void 0 ? _b : Date.now(); };
@@ -25311,6 +26406,33 @@ class DepositFlow extends Flow {
25311
26406
  this._accept = null;
25312
26407
  bridge.settle({ kind: 'declined' });
25313
26408
  }
26409
+ /**
26410
+ * Batch path: authorise the single transaction that approves and deposits,
26411
+ * and resume the parked run. A no-op when nothing is parked on
26412
+ * `awaiting-authorise`, so a double tap cannot submit twice.
26413
+ *
26414
+ * The batch has no wallet prompt per call, so this is the lender's
26415
+ * explicit consent to the transfer — call it only from their action.
26416
+ */
26417
+ authoriseTransfer() {
26418
+ const bridge = this._authorise;
26419
+ if (!bridge)
26420
+ return;
26421
+ this._authorise = null;
26422
+ bridge.settle({ kind: 'authorised' });
26423
+ }
26424
+ /**
26425
+ * Batch path: refuse the transfer. The run ends on `error` with `{ step:
26426
+ * 'request', reason: 'cancelled' }` and nothing is submitted. A no-op when
26427
+ * nothing is parked on `awaiting-authorise`.
26428
+ */
26429
+ declineTransfer() {
26430
+ const bridge = this._authorise;
26431
+ if (!bridge)
26432
+ return;
26433
+ this._authorise = null;
26434
+ bridge.settle({ kind: 'declined' });
26435
+ }
25314
26436
  /** True only while THIS generation is waiting on the acceptance signature. */
25315
26437
  _isAccepting() {
25316
26438
  return (this._acceptingFor !== null &&
@@ -25322,6 +26444,9 @@ class DepositFlow extends Flow {
25322
26444
  this._accept = null;
25323
26445
  this._acceptingFor = null;
25324
26446
  bridge === null || bridge === void 0 ? void 0 : bridge.settle({ kind: 'abandoned' });
26447
+ const authorise = this._authorise;
26448
+ this._authorise = null;
26449
+ authorise === null || authorise === void 0 ? void 0 : authorise.settle({ kind: 'abandoned' });
25325
26450
  }
25326
26451
  // -----------------------------------------------------------------------
25327
26452
  _run(input, token) {
@@ -25329,6 +26454,7 @@ class DepositFlow extends Flow {
25329
26454
  var _a;
25330
26455
  this._accept = null;
25331
26456
  this._acceptingFor = null;
26457
+ this._authorise = null;
25332
26458
  const ports = this._ports;
25333
26459
  const owner = input.userAddress.toLowerCase();
25334
26460
  // 0. The spender, from the input or from the chain config the facade
@@ -25349,21 +26475,32 @@ class DepositFlow extends Flow {
25349
26475
  // live, never cached: an exact-amount approval is fully consumed by
25350
26476
  // the deposit it paid for, so a stale allowance is exactly the value
25351
26477
  // that would wrongly skip the approve and revert the deposit.
25352
- let approvalRequired = true;
26478
+ let allowanceShort = true;
25353
26479
  try {
25354
26480
  const allowance = yield ports.readAllowance(owner, spender);
25355
- approvalRequired = allowance.lt(input.amount);
26481
+ allowanceShort = allowance.lt(input.amount);
25356
26482
  }
25357
26483
  catch (_b) {
25358
26484
  // A read failure is not a reason to skip an approval. Assume one is
25359
26485
  // needed: the cost is a redundant approve, the alternative is a
25360
26486
  // reverted deposit.
25361
- approvalRequired = true;
26487
+ allowanceShort = true;
25362
26488
  }
25363
26489
  if (!this._store.isCurrent(token))
25364
26490
  return;
26491
+ // The path is chosen HERE, with the allowance, so the badge is right
26492
+ // from the first state the lender sees. Batch only when there is an
26493
+ // approval to fold in, a port to submit it, and a known token to
26494
+ // approve on — never a guessed one. A batched run never enters the
26495
+ // `approve` step, so every badge counts it as a three-step run.
26496
+ const stableAsset = this._stableAsset;
26497
+ const batched = allowanceShort &&
26498
+ typeof ports.depositBatch === 'function' &&
26499
+ !!stableAsset;
26500
+ const approvalRequired = allowanceShort && !batched;
25365
26501
  // 2. Generate — sign the auth message.
25366
- this._store.patch(Object.assign({ approvalRequired, phase: 'generating-sign' }, badgeFor('generate', approvalRequired)), token);
26502
+ this._store.patch(Object.assign({ approvalRequired,
26503
+ batched, phase: 'generating-sign' }, badgeFor('generate', approvalRequired)), token);
25367
26504
  const timestamp = this._now();
25368
26505
  const signedMessage = buildAuthMessage(input, owner, timestamp);
25369
26506
  let signature;
@@ -25465,6 +26602,18 @@ class DepositFlow extends Flow {
25465
26602
  });
25466
26603
  return;
25467
26604
  }
26605
+ if (batched && stableAsset) {
26606
+ yield this._runBatch({
26607
+ token,
26608
+ input,
26609
+ owner,
26610
+ spender,
26611
+ stableAsset,
26612
+ depositData,
26613
+ contract,
26614
+ });
26615
+ return;
26616
+ }
25468
26617
  // 6. Approve — the EXACT amount, never `MaxUint256`. House rule: an
25469
26618
  // unlimited allowance outlives the deposit it was granted for, and a
25470
26619
  // later exploit of the spender would drain a wallet that has long
@@ -25528,6 +26677,108 @@ class DepositFlow extends Flow {
25528
26677
  this._store.patch(Object.assign({ phase: 'success' }, badgeFor('request', approvalRequired)), token);
25529
26678
  });
25530
26679
  }
26680
+ /**
26681
+ * The batch path, from the accepted agreement to the receipt.
26682
+ *
26683
+ * 1. PARK on `awaiting-authorise`. A batch puts no wallet prompt in front
26684
+ * of each call, so the flow waits for an explicit authorisation before
26685
+ * any money moves. `declineTransfer()` ends it as `cancelled`.
26686
+ * 2. Re-check the TTL: the lender may have idled on the authorisation as
26687
+ * long as they did on the agreement.
26688
+ * 3. Only now fetch the KYC signature — it has a block expiration of its
26689
+ * own, and an authorisation left open must not spend it.
26690
+ * 4. Encode both calls and hand them to `depositBatch`; then the receipt.
26691
+ */
26692
+ _runBatch(args) {
26693
+ return __awaiter(this, void 0, void 0, function* () {
26694
+ const { token, input, owner, spender, stableAsset, depositData } = args;
26695
+ const ports = this._ports;
26696
+ // A batched run is a three-step run on every badge.
26697
+ const approvalRequired = false;
26698
+ // Every settler clears the handshake before settling it, so there is
26699
+ // nothing to clear here — and clearing it anyway could drop the
26700
+ // handshake of a NEWER run that `reset(); start()` already parked.
26701
+ const outcome = yield new Promise((resolve) => {
26702
+ this._authorise = { settle: resolve };
26703
+ this._store.patch(Object.assign({ phase: 'awaiting-authorise' }, badgeFor('request', approvalRequired)), token);
26704
+ });
26705
+ if (!this._store.isCurrent(token))
26706
+ return;
26707
+ if (outcome.kind === 'declined') {
26708
+ this._fail(token, approvalRequired, {
26709
+ step: 'request',
26710
+ reason: 'cancelled',
26711
+ });
26712
+ return;
26713
+ }
26714
+ if (outcome.kind !== 'authorised')
26715
+ return;
26716
+ if (this._now() > args.contract.timestamp + this._ttlMs) {
26717
+ this._fail(token, approvalRequired, {
26718
+ step: 'request',
26719
+ reason: 'contract-expired',
26720
+ });
26721
+ return;
26722
+ }
26723
+ this._store.patch(Object.assign({ phase: 'request-sign' }, badgeFor('request', approvalRequired)), token);
26724
+ // The consumer's backend, so `failed` — never `cancelled` — exactly as
26725
+ // on the two-transaction path.
26726
+ let kyc;
26727
+ try {
26728
+ const kycParams = yield ports.buildKycParams(owner);
26729
+ kyc = yield ports.getKycSignature(kycParams);
26730
+ }
26731
+ catch (err) {
26732
+ this._fail(token, approvalRequired, {
26733
+ step: 'request',
26734
+ reason: 'failed',
26735
+ error: err,
26736
+ });
26737
+ return;
26738
+ }
26739
+ if (!this._store.isCurrent(token))
26740
+ return;
26741
+ // Encoded from values a backend returned (the KYC signature), so a
26742
+ // shape the coder refuses is a failure the consumer can render, not a
26743
+ // rejection out of a `start()` that promises it has none.
26744
+ let calls;
26745
+ try {
26746
+ calls = encodeDepositBatch({
26747
+ stableAsset,
26748
+ spender,
26749
+ input,
26750
+ depositData,
26751
+ kyc,
26752
+ });
26753
+ }
26754
+ catch (err) {
26755
+ this._fail(token, approvalRequired, {
26756
+ step: 'request',
26757
+ reason: 'failed',
26758
+ error: err,
26759
+ });
26760
+ return;
26761
+ }
26762
+ try {
26763
+ // Called through the ports object, so a port that is a method
26764
+ // keeps its `this`. Its presence decided the path; a consumer
26765
+ // that removed it since gets a `failed`, not a crash.
26766
+ if (typeof ports.depositBatch !== 'function') {
26767
+ throw new Error('DepositFlow: the depositBatch port is gone');
26768
+ }
26769
+ const tx = yield ports.depositBatch(calls);
26770
+ this._store.patch(Object.assign({ phase: 'request-confirm' }, badgeFor('request', approvalRequired)), token);
26771
+ yield tx.wait();
26772
+ }
26773
+ catch (err) {
26774
+ this._fail(token, approvalRequired, classifyBatch(err));
26775
+ return;
26776
+ }
26777
+ if (!this._store.isCurrent(token))
26778
+ return;
26779
+ this._store.patch(Object.assign({ phase: 'success' }, badgeFor('request', approvalRequired)), token);
26780
+ });
26781
+ }
25531
26782
  _fail(token, approvalRequired, failure) {
25532
26783
  this._store.patch(Object.assign({ phase: 'error', failure }, badgeFor(failure.step, approvalRequired)), token);
25533
26784
  }
@@ -25574,16 +26825,50 @@ const INITIAL = {
25574
26825
  phase: 'idle',
25575
26826
  step: null,
25576
26827
  isMax: false,
26828
+ sponsored: false,
25577
26829
  failure: null,
25578
26830
  };
25579
26831
  /** What a KYC pre-check with no `buildKycParams` to call fails with. */
25580
26832
  const NO_KYC_PARAMS_MESSAGE = 'WithdrawFlow: getKycSignature was supplied without buildKycParams; build the flow with kasu.flows.withdraw() or pass both';
26833
+ /** What a sponsored run with no `encodeWithdrawal` to call fails with. */
26834
+ const NO_WITHDRAWAL_ENCODER_MESSAGE = 'WithdrawFlow: submitCall was supplied without encodeWithdrawal; build the flow with kasu.flows.withdraw() or pass both';
26835
+ /** What a sponsored run fails with when the encoder returned another kind of call. */
26836
+ const NOT_A_WITHDRAWAL_CALL_MESSAGE = 'WithdrawFlow: encodeWithdrawal returned a call that is not a withdrawal';
26837
+ /**
26838
+ * A `submitCall` / `wait()` rejection, read with the batch path's taxonomy. A
26839
+ * submission outcome is taken at its word FIRST — never through the wallet
26840
+ * classifier, which would read a relay's "request rejected" as the lender's
26841
+ * own doing. Then, from `submitCall` only, a wallet rejection is `cancelled`:
26842
+ * the lender declined to sign and nothing was posted — the same reading a raw
26843
+ * `depositBatch` throw gets. From `wait()` the call has already left, so a
26844
+ * rejection-worded error there is as ambiguous as any other. Everything
26845
+ * unrecognised is `pending`: it may have left.
26846
+ */
26847
+ function classifySponsored(err, from) {
26848
+ if (err instanceof BatchSubmissionError) {
26849
+ return err.outcome === 'failed'
26850
+ ? { step: 'request', reason: 'failed', error: err }
26851
+ : { step: 'request', reason: err.outcome, error: err };
26852
+ }
26853
+ if (err instanceof BatchRevertedError) {
26854
+ return {
26855
+ step: 'request',
26856
+ reason: 'reverted',
26857
+ revertError: err.revertError,
26858
+ error: err,
26859
+ };
26860
+ }
26861
+ if (from === 'submit' && isUserRejected(err)) {
26862
+ return { step: 'request', reason: 'cancelled' };
26863
+ }
26864
+ return { step: 'request', reason: 'pending', error: err };
26865
+ }
25581
26866
  // ---------------------------------------------------------------------------
25582
26867
  // The flow
25583
26868
  // ---------------------------------------------------------------------------
25584
26869
  class WithdrawFlow extends Flow {
25585
26870
  constructor(_ports) {
25586
- super(INITIAL);
26871
+ super(Object.assign(Object.assign({}, INITIAL), { sponsored: typeof _ports.submitCall === 'function' }));
25587
26872
  this._ports = _ports;
25588
26873
  }
25589
26874
  _run(input, token) {
@@ -25614,6 +26899,10 @@ class WithdrawFlow extends Flow {
25614
26899
  if (!this._store.isCurrent(token))
25615
26900
  return;
25616
26901
  }
26902
+ if (ports.submitCall) {
26903
+ yield this._runSponsored(input, token, isMax);
26904
+ return;
26905
+ }
25617
26906
  // 2. The submission. This one IS a wallet call, so it keeps the
25618
26907
  // `cancelled` / `failed` split.
25619
26908
  this._store.patch({ phase: 'request-sign', step: 'request', isMax }, token);
@@ -25640,6 +26929,72 @@ class WithdrawFlow extends Flow {
25640
26929
  this._store.patch({ phase: 'success' }, token);
25641
26930
  });
25642
26931
  }
26932
+ /**
26933
+ * 2'. The sponsored submission: encode, re-check the run, submit, settle.
26934
+ *
26935
+ * The encode reads the chain (the shares), so a run abandoned during it
26936
+ * must stop BEFORE `submitCall` — nothing has been sent yet, and an
26937
+ * abandoned run must never send. A throw while encoding is `failed`:
26938
+ * nothing left. After `submitCall` is called, only the taxonomy decides.
26939
+ */
26940
+ _runSponsored(input, token, isMax) {
26941
+ return __awaiter(this, void 0, void 0, function* () {
26942
+ const ports = this._ports;
26943
+ // Only reached when `submitCall` is supplied; narrows it for below.
26944
+ if (!ports.submitCall)
26945
+ return;
26946
+ this._store.patch({ phase: 'request-sign', step: 'request', isMax }, token);
26947
+ let call;
26948
+ try {
26949
+ if (!ports.encodeWithdrawal) {
26950
+ throw new Error(NO_WITHDRAWAL_ENCODER_MESSAGE);
26951
+ }
26952
+ call = yield ports.encodeWithdrawal({
26953
+ poolId: input.poolId,
26954
+ trancheId: input.trancheId,
26955
+ amount: input.amount,
26956
+ userAddress: input.userAddress,
26957
+ });
26958
+ if ((call === null || call === void 0 ? void 0 : call.kind) !== 'withdrawal') {
26959
+ throw new Error(NOT_A_WITHDRAWAL_CALL_MESSAGE);
26960
+ }
26961
+ }
26962
+ catch (err) {
26963
+ this._store.patch({
26964
+ phase: 'error',
26965
+ failure: { step: 'request', reason: 'failed', error: err },
26966
+ }, token);
26967
+ return;
26968
+ }
26969
+ if (!this._store.isCurrent(token))
26970
+ return;
26971
+ let tx;
26972
+ try {
26973
+ tx = yield ports.submitCall(call);
26974
+ }
26975
+ catch (err) {
26976
+ this._store.patch({ phase: 'error', failure: classifySponsored(err, 'submit') }, token);
26977
+ return;
26978
+ }
26979
+ // The call has left. A run abandoned meanwhile stops HERE, without
26980
+ // `wait()`: nothing it could learn would be published, and a sponsored
26981
+ // `wait()` is minutes of polling against the submitter, not a cheap
26982
+ // receipt read. The submission itself cannot be taken back.
26983
+ if (!this._store.isCurrent(token))
26984
+ return;
26985
+ this._store.patch({ phase: 'request-confirm' }, token);
26986
+ try {
26987
+ yield tx.wait();
26988
+ }
26989
+ catch (err) {
26990
+ this._store.patch({ phase: 'error', failure: classifySponsored(err, 'wait') }, token);
26991
+ return;
26992
+ }
26993
+ if (!this._store.isCurrent(token))
26994
+ return;
26995
+ this._store.patch({ phase: 'success' }, token);
26996
+ });
26997
+ }
25643
26998
  }
25644
26999
 
25645
27000
  /**
@@ -25680,11 +27035,26 @@ class FlowsFacade {
25680
27035
  * an address whose only wrong value grants an approval to the wrong
25681
27036
  * contract.
25682
27037
  */
25683
- _spender) {
27038
+ _spender,
27039
+ /**
27040
+ * This chain's stable token address — the target of the batch path's
27041
+ * approve. `undefined` for a hand-built config without `stableAsset`,
27042
+ * in which case a run never batches: it does not guess a token.
27043
+ */
27044
+ _stableAsset,
27045
+ /**
27046
+ * This instance's call encoders — the sponsored withdraw path's
27047
+ * default `encodeWithdrawal`. Optional so a hand-built facade keeps
27048
+ * constructing; without it a sponsored run fails (`failed`, nothing
27049
+ * sent) unless the consumer passes `encodeWithdrawal` itself.
27050
+ */
27051
+ _calls) {
25684
27052
  this._deposits = _deposits;
25685
27053
  this._erc20 = _erc20;
25686
27054
  this._isReadOnly = _isReadOnly;
25687
27055
  this._spender = _spender;
27056
+ this._stableAsset = _stableAsset;
27057
+ this._calls = _calls;
25688
27058
  }
25689
27059
  /**
25690
27060
  * A deposit pipeline. `readAllowance`, `approve`, `deposit` and
@@ -25697,9 +27067,13 @@ class FlowsFacade {
25697
27067
  * delete the default it was meant to keep, and
25698
27068
  * `approve: sponsoredGas ? sponsoredApprove : undefined` is exactly how a
25699
27069
  * consumer writes a conditional override.
27070
+ *
27071
+ * `depositBatch` has NO default: how several calls become one atomic
27072
+ * transaction is the application's business. Supplying it lets a run
27073
+ * whose allowance is short approve and deposit in one batch.
25700
27074
  */
25701
27075
  deposit(ports, opts) {
25702
- var _a, _b, _c, _d, _e;
27076
+ var _a, _b, _c, _d, _e, _f;
25703
27077
  return new DepositFlow({
25704
27078
  signMessage: ports.signMessage,
25705
27079
  generateContract: ports.generateContract,
@@ -25714,21 +27088,26 @@ class FlowsFacade {
25714
27088
  return yield this._erc20().approve(spender, amount);
25715
27089
  })),
25716
27090
  deposit: (_d = ports.deposit) !== null && _d !== void 0 ? _d : ((params) => this._deposits.deposit(params)),
27091
+ depositBatch: ports.depositBatch,
25717
27092
  now: ports.now,
25718
27093
  }, {
25719
27094
  contractTtlMs: opts === null || opts === void 0 ? void 0 : opts.contractTtlMs,
25720
27095
  // Per key here too, for the same reason the ports are: an
25721
27096
  // explicit `spender: undefined` must not delete the default.
25722
27097
  spender: (_e = opts === null || opts === void 0 ? void 0 : opts.spender) !== null && _e !== void 0 ? _e : this._spender,
27098
+ stableAsset: (_f = opts === null || opts === void 0 ? void 0 : opts.stableAsset) !== null && _f !== void 0 ? _f : this._stableAsset,
25723
27099
  });
25724
27100
  }
25725
27101
  /**
25726
27102
  * A withdrawal pipeline. Both write ports and `buildKycParams` default to
25727
27103
  * this instance; supplying `getKycSignature` turns the KYC pre-check on.
27104
+ * Supplying `submitCall` turns the sponsored path on, and its encoder
27105
+ * defaults to `kasu.calls.requestWithdrawal` on this instance — which
27106
+ * reads, never writes, so it works on a read-only instance too.
25728
27107
  * Defaults are applied per key, for the reason `deposit()` gives.
25729
27108
  */
25730
27109
  withdraw(ports = {}) {
25731
- var _a, _b, _c;
27110
+ var _a, _b, _c, _d;
25732
27111
  return new WithdrawFlow({
25733
27112
  buildKycParams: (_a = ports.buildKycParams) !== null && _a !== void 0 ? _a : ((userAddress) => this._deposits.buildKycParams(userAddress)),
25734
27113
  getKycSignature: ports.getKycSignature,
@@ -25738,8 +27117,17 @@ class FlowsFacade {
25738
27117
  amount: params.amount,
25739
27118
  })),
25740
27119
  withdrawMax: (_c = ports.withdrawMax) !== null && _c !== void 0 ? _c : ((poolId, trancheId, userAddress) => this._deposits.withdrawMax(poolId, trancheId, userAddress)),
27120
+ submitCall: ports.submitCall,
27121
+ encodeWithdrawal: (_d = ports.encodeWithdrawal) !== null && _d !== void 0 ? _d : this._defaultEncoder(),
25741
27122
  });
25742
27123
  }
27124
+ /** `kasu.calls.requestWithdrawal`, when this facade was given the encoders. */
27125
+ _defaultEncoder() {
27126
+ const calls = this._calls;
27127
+ if (!calls)
27128
+ return undefined;
27129
+ return (params) => calls.requestWithdrawal(params);
27130
+ }
25743
27131
  _assertWritable() {
25744
27132
  if (this._isReadOnly) {
25745
27133
  throw new Error(READ_ONLY_MESSAGE);
@@ -26034,9 +27422,72 @@ function maxNetRateCeiling(pools, feePercent) {
26034
27422
  * Handles epoch fetching internally so callers never need to know about epochs.
26035
27423
  */
26036
27424
  class StrategiesFacade {
26037
- constructor(_dataService, _userLending) {
27425
+ constructor(_dataService, _userLending,
27426
+ /**
27427
+ * Chain pool id → the pool id its CMS record is keyed by (XDC pools
27428
+ * share their Base counterpart's editorial record). Lowercase keys.
27429
+ */
27430
+ _poolMetadataMapping = {}) {
26038
27431
  this._dataService = _dataService;
26039
27432
  this._userLending = _userLending;
27433
+ this._poolMetadataMapping = _poolMetadataMapping;
27434
+ }
27435
+ /**
27436
+ * The pool's loss rate and first-loss capital, from its CMS
27437
+ * risk-management record. `null` when the pool has no record.
27438
+ *
27439
+ * Each figure is a NUMBER or `null` — never a guess. The CMS returns
27440
+ * "numeric" fields as a number, a numeric string, an empty string or
27441
+ * null, and an empty string must not become a confident `0`: anything that
27442
+ * is not a finite number reads as `null` (render "—"). `poolLossRate` is a
27443
+ * 0..1 fraction.
27444
+ *
27445
+ * Requires `directusUrl`; without it the call rejects with
27446
+ * `NO_DIRECTUS_URL_MESSAGE`, like every CMS-only read.
27447
+ */
27448
+ getRiskPerformance(poolId) {
27449
+ return __awaiter(this, void 0, void 0, function* () {
27450
+ const metadataId = this._metadataIdOf(poolId);
27451
+ const records = yield this._dataService.getRiskManagement();
27452
+ const record = records.find((r) => String(r.poolIdFK).toLowerCase() === metadataId);
27453
+ if (!record)
27454
+ return null;
27455
+ const performance = record.riskPerformance;
27456
+ return {
27457
+ poolLossRate: finiteOrNull(performance === null || performance === void 0 ? void 0 : performance.poolLossRate),
27458
+ firstLossCapital: finiteOrNull(performance === null || performance === void 0 ? void 0 : performance.firstLossCapital),
27459
+ };
27460
+ });
27461
+ }
27462
+ /**
27463
+ * The credit originator's profile and lending history for a pool — the
27464
+ * CMS record whose `otherKASUPools` lists it. `null` when none does.
27465
+ *
27466
+ * Matched on the chain pool id, then on its metadata id, case-
27467
+ * insensitively. The record is returned as the service maps it; its
27468
+ * numeric fields are CMS-authored, so read them defensively.
27469
+ *
27470
+ * The service filters `otherKASUPools` through the CURRENT chain's
27471
+ * subgraph with the CMS record's (Base-keyed) ids, so on an XDC
27472
+ * deployment every profile lists no pools and this resolves `null` —
27473
+ * the same answer kasu-ui's own lookup gives there today. The metadata
27474
+ * candidate becomes live once the service resolves the mapping.
27475
+ */
27476
+ getOriginator(poolId) {
27477
+ return __awaiter(this, void 0, void 0, function* () {
27478
+ var _a;
27479
+ const candidates = new Set([
27480
+ poolId.toLowerCase(),
27481
+ this._metadataIdOf(poolId),
27482
+ ]);
27483
+ const profiles = yield this._dataService.getPoolDelegateProfileAndHistory();
27484
+ return ((_a = profiles.find((profile) => profile.otherKASUPools.some((pool) => candidates.has(String(pool.id).toLowerCase())))) !== null && _a !== void 0 ? _a : null);
27485
+ });
27486
+ }
27487
+ _metadataIdOf(poolId) {
27488
+ var _a;
27489
+ const lower = poolId.toLowerCase();
27490
+ return ((_a = this._poolMetadataMapping[lower]) !== null && _a !== void 0 ? _a : lower).toLowerCase();
26040
27491
  }
26041
27492
  /**
26042
27493
  * Fetch all active lending strategies.
@@ -26115,9 +27566,7 @@ class StrategiesFacade {
26115
27566
  */
26116
27567
  calculateDepositLimits(tranche) {
26117
27568
  const raw = tranche._raw;
26118
- const min = parseFloat(raw.minimumDeposit) < 1
26119
- ? '1'
26120
- : raw.minimumDeposit;
27569
+ const min = parseFloat(raw.minimumDeposit) < 1 ? '1' : raw.minimumDeposit;
26121
27570
  const max = parseFloat(raw.maximumDeposit) > parseFloat(raw.poolCapacity)
26122
27571
  ? raw.poolCapacity
26123
27572
  : raw.maximumDeposit;
@@ -26165,14 +27614,146 @@ class StrategiesFacade {
26165
27614
  };
26166
27615
  }
26167
27616
  }
27617
+ /**
27618
+ * A CMS "numeric" field as a finite number, or `null`. An empty or blank
27619
+ * string is UNAUTHORED, not zero.
27620
+ */
27621
+ function finiteOrNull(value) {
27622
+ if (typeof value === 'number')
27623
+ return Number.isFinite(value) ? value : null;
27624
+ if (typeof value === 'string') {
27625
+ if (value.trim() === '')
27626
+ return null;
27627
+ const numeric = Number(value);
27628
+ return Number.isFinite(numeric) ? numeric : null;
27629
+ }
27630
+ return null;
27631
+ }
27632
+
27633
+ var UserRequestStatus;
27634
+ (function (UserRequestStatus) {
27635
+ UserRequestStatus["REQUESTED"] = "Requested";
27636
+ UserRequestStatus["PROCESSING"] = "Processing";
27637
+ UserRequestStatus["PROCESSED"] = "Processed";
27638
+ })(UserRequestStatus || (UserRequestStatus = {}));
27639
+
27640
+ /**
27641
+ * Cycles and the clearing window — the protocol's weekly clock, as numbers.
27642
+ *
27643
+ * Lifted verbatim from kasu-ui's `features/portfolio/lib/settlement-window.ts`
27644
+ * and `features/lending/lib/cycle-dates.ts`. The two formatters that live
27645
+ * beside `deriveCycleDates` there (`formatCycleDate`, `formatCycleCloseUtc`)
27646
+ * print words and stay in the applications; everything here is unix seconds in
27647
+ * and unix seconds out.
27648
+ *
27649
+ * The clearing window is the fixed 48 hours immediately preceding an epoch
27650
+ * end. Inside it, pending requests are being processed and cannot be modified,
27651
+ * and the countdown runs to the epoch end. Outside it, the countdown runs to
27652
+ * the next clearing-window start.
27653
+ *
27654
+ * The epoch end comes straight from the chain (`nextEpochStartTimestamp`, i.e.
27655
+ * the SDK's `getNextEpochDate`) — the same value the protocol's own
27656
+ * `getNextClearingPeriodDate` derives from — so the window always lines up
27657
+ * with the real weekly schedule (Tue 06:00 → Thu 06:00 UTC on Base) instead of
27658
+ * a projected subgraph timestamp that can drift off the grid.
27659
+ */
27660
+ /**
27661
+ * Clearing-window length — a fixed 48h protocol constant. Exported so every
27662
+ * consumer derives the window from the same number this module runs on.
27663
+ */
27664
+ const CLEARING_WINDOW_SECONDS = 48 * 60 * 60;
27665
+ /**
27666
+ * Weekly cadence — the epoch schedule is fixed weekly (Tue → Thu UTC on Base).
27667
+ * Used only to roll a cycle forward when a request lands inside a window that
27668
+ * has already closed.
27669
+ */
27670
+ const WEEK_SECONDS = 7 * 24 * 60 * 60;
27671
+ /**
27672
+ * Which phase of the weekly cycle `nowSeconds` falls in, and how long is left
27673
+ * of it.
27674
+ *
27675
+ * `'unknown'` when no epoch boundary has been loaded yet, or when the one on
27676
+ * hand is stale (it has already elapsed — the on-chain value refetches to the
27677
+ * next boundary shortly after rollover). A caller must render its "no cycle
27678
+ * loaded" state there, never a zeroed countdown.
27679
+ */
27680
+ function computeSettlementWindow({ nowSeconds, nextEpochStart, clearingWindowSeconds = CLEARING_WINDOW_SECONDS, }) {
27681
+ // No epoch boundary loaded yet, or a stale one that already elapsed.
27682
+ if (nextEpochStart <= 0 || nextEpochStart <= nowSeconds) {
27683
+ return { phase: 'unknown' };
27684
+ }
27685
+ const clearingStart = nextEpochStart - clearingWindowSeconds;
27686
+ if (nowSeconds < clearingStart) {
27687
+ return {
27688
+ phase: 'awaiting',
27689
+ secondsUntilClearing: clearingStart - nowSeconds,
27690
+ nextClearingStart: clearingStart,
27691
+ };
27692
+ }
27693
+ // nowSeconds is in [clearingStart, nextEpochStart) — inside the window.
27694
+ return {
27695
+ phase: 'clearing',
27696
+ secondsUntilEpochEnd: nextEpochStart - nowSeconds,
27697
+ epochEnd: nextEpochStart,
27698
+ };
27699
+ }
27700
+ /**
27701
+ * The next cycle boundary strictly after `nowSeconds`, in unix seconds — the
27702
+ * cycle close (`nextEpochStart − 48h`) while the window is still open, the
27703
+ * epoch end once we are inside it.
27704
+ *
27705
+ * `undefined` when there is no boundary left to wait for: no epoch boundary
27706
+ * loaded, or a cached one that has already elapsed — the same staleness rule
27707
+ * `computeSettlementWindow` applies before it reports `'unknown'`.
27708
+ *
27709
+ * Split out of the state machine because some consumers need the INSTANT
27710
+ * rather than the phase: one to flush the cycle-dependent caches when the
27711
+ * clock crosses it, one to move a pre-commit screen's snapshot clock at the
27712
+ * same moment.
27713
+ */
27714
+ function nextCycleBoundary(nextEpochStart, nowSeconds, clearingWindowSeconds = CLEARING_WINDOW_SECONDS) {
27715
+ if (!nextEpochStart || nextEpochStart <= 0)
27716
+ return undefined;
27717
+ const clearingStart = nextEpochStart - clearingWindowSeconds;
27718
+ if (nowSeconds < clearingStart)
27719
+ return clearingStart;
27720
+ if (nowSeconds < nextEpochStart)
27721
+ return nextEpochStart;
27722
+ return undefined;
27723
+ }
27724
+ /**
27725
+ * The cycle-close and outcome dates for a request submitted `now`.
27726
+ *
27727
+ * - The cycle "closes" (stops accepting requests, starts processing) at the
27728
+ * start of the 48h clearing window, i.e. 48h before the epoch end.
27729
+ * - Processing takes up to 48h, so the outcome is confirmed by the epoch end
27730
+ * (close + 48h).
27731
+ *
27732
+ * Returns `null` when the epoch boundary is not available or is stale — a
27733
+ * caller then omits the dates entirely (omit, don't stub).
27734
+ *
27735
+ * The common case is a request submitted OUTSIDE the clearing window: the
27736
+ * close is `nextEpochStart − 48h` and the outcome is `nextEpochStart`. When
27737
+ * the request lands INSIDE the current clearing window (that close is already
27738
+ * in the past), it queues for the NEXT weekly cycle, so the close is advanced
27739
+ * by whole weeks until it is in the future.
27740
+ */
27741
+ function deriveCycleDates(nextEpochStart, nowSeconds) {
27742
+ // No boundary loaded, or a stale one that already elapsed — the same
27743
+ // staleness rule as `computeSettlementWindow`.
27744
+ if (!nextEpochStart || nextEpochStart <= nowSeconds)
27745
+ return null;
27746
+ let close = nextEpochStart - CLEARING_WINDOW_SECONDS;
27747
+ let outcome = nextEpochStart;
27748
+ // Inside the current clearing window the close already passed; a request
27749
+ // now is queued for the next weekly cycle.
27750
+ while (close <= nowSeconds) {
27751
+ close += WEEK_SECONDS;
27752
+ outcome += WEEK_SECONDS;
27753
+ }
27754
+ return { close, outcome };
27755
+ }
26168
27756
 
26169
- var UserRequestStatus;
26170
- (function (UserRequestStatus) {
26171
- UserRequestStatus["REQUESTED"] = "Requested";
26172
- UserRequestStatus["PROCESSING"] = "Processing";
26173
- UserRequestStatus["PROCESSED"] = "Processed";
26174
- })(UserRequestStatus || (UserRequestStatus = {}));
26175
-
26176
27757
  /**
26177
27758
  * Events that represent a lender SUBMISSION into the bundle. A dNFT position
26178
27759
  * aggregates every submission the lender made into the same pool/tranche this
@@ -26233,6 +27814,38 @@ function lastEventTimestamp(events, fallback) {
26233
27814
  function isCycleClosed(request) {
26234
27815
  return !request.canCancel;
26235
27816
  }
27817
+ /**
27818
+ * Until when can THIS request still be cancelled? — unix seconds, or `null`
27819
+ * when it cannot be cancelled at all, or when no epoch boundary is loaded.
27820
+ *
27821
+ * The answer is the next cycle close (`deriveCycleDates(...).close`): the
27822
+ * clearing window opens there, and the contract refuses a cancel while the
27823
+ * pool's clearing is pending (`CannotCancelRequestIfClearingIsPending`).
27824
+ * Inside a window whose clearing already ENDED, a still-cancellable request is
27825
+ * queued for the next cycle, so its deadline is next week's close — which is
27826
+ * exactly what `deriveCycleDates` rolls forward to.
27827
+ *
27828
+ * `null` unless the request is BOTH:
27829
+ *
27830
+ * - `canCancel` — the SDK's per-pool signal (see `isCycleClosed`);
27831
+ * - live: `statusCode` `pending`, or `partial` (a partly-filled withdrawal
27832
+ * whose remainder is back in the queue — a resolved partial has
27833
+ * `canCancel: false`). A cancelled, forced, reallocated, rejected or complete
27834
+ * request has nothing left to cancel, whatever `canCancel` says.
27835
+ *
27836
+ * `canCancel` is read ONCE, when the request was fetched. A request held
27837
+ * across a cycle boundary (`nextCycleBoundary`) must be refetched before this
27838
+ * is shown again — the deadline is a hint, the contract is the gate.
27839
+ */
27840
+ function cancelDeadline(request, nextEpochStart, nowSeconds) {
27841
+ var _a, _b;
27842
+ if (!request.canCancel)
27843
+ return null;
27844
+ if (request.statusCode !== 'pending' && request.statusCode !== 'partial') {
27845
+ return null;
27846
+ }
27847
+ return (_b = (_a = deriveCycleDates(nextEpochStart, nowSeconds)) === null || _a === void 0 ? void 0 : _a.close) !== null && _b !== void 0 ? _b : null;
27848
+ }
26236
27849
  const isCancelled = (events) => events.some((e) => e.requestType === 'Cancelled');
26237
27850
  const isForced = (events) => events.some((e) => e.requestType === 'Forced');
26238
27851
  const initiatedEvent = (events) => events.find((e) => e.requestType === 'Initiated');
@@ -26459,8 +28072,8 @@ class PortfolioFacade {
26459
28072
  /**
26460
28073
  * High-level entry point for external integrators.
26461
28074
  *
26462
- * Provides four domain facades — `strategies`, `deposits`, `portfolio`,
26463
- * `flows` — and exposes the underlying `KasuSdk` services via `.services` for
28075
+ * Provides five domain facades — `strategies`, `deposits`, `portfolio`,
28076
+ * `flows`, `calls` — and exposes the underlying `KasuSdk` services via `.services` for
26464
28077
  * power-users.
26465
28078
  *
26466
28079
  * ```ts
@@ -26480,18 +28093,22 @@ class PortfolioFacade {
26480
28093
  */
26481
28094
  class Kasu {
26482
28095
  constructor(sdk, chainConfig, signerOrProvider, configOverrides) {
28096
+ var _a, _b;
26483
28097
  this._sdk = sdk;
26484
28098
  this._chainConfig = chainConfig;
26485
28099
  this._signerOrProvider = signerOrProvider;
26486
28100
  this._configOverrides = configOverrides;
26487
- this.strategies = new StrategiesFacade(sdk.DataService, sdk.UserLending);
28101
+ this.strategies = new StrategiesFacade(sdk.DataService, sdk.UserLending, normaliseMapping((_a = configOverrides.poolMetadataMapping) !== null && _a !== void 0 ? _a : chainConfig.poolMetadataMapping));
26488
28102
  // Derived ONCE and handed to every facade that needs it. Each of them
26489
28103
  // re-deriving `Signer.isSigner` is how two facades come to disagree
26490
28104
  // about whether the same instance can write.
26491
28105
  const isReadOnly = !ethers.Signer.isSigner(signerOrProvider);
26492
28106
  this.deposits = new DepositsFacade(sdk.UserLending, chainConfig.chainId.toString(), isReadOnly);
26493
28107
  this.portfolio = new PortfolioFacade(sdk.DataService, sdk.UserLending, sdk.Portfolio);
26494
- this.flows = new FlowsFacade(this.deposits, () => erc20Of(chainConfig, signerOrProvider), isReadOnly, chainConfig.contracts.LendingPoolManager);
28108
+ // The same `LendingPoolManager` the deposit flow approves and calls:
28109
+ // the chain config's, which is what a relay allow-lists `to` against.
28110
+ this.calls = new CallsFacade(sdk.UserLending, chainConfig.contracts.LendingPoolManager);
28111
+ 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);
26495
28112
  }
26496
28113
  /**
26497
28114
  * Create a Kasu instance with built-in chain config.
@@ -26676,6 +28293,14 @@ function resolveChainConfig(chain) {
26676
28293
  }
26677
28294
  return chain;
26678
28295
  }
28296
+ /** Lowercase both sides, so a lookup by any casing of a pool id matches. */
28297
+ function normaliseMapping(mapping) {
28298
+ const out = {};
28299
+ for (const [from, to] of Object.entries(mapping !== null && mapping !== void 0 ? mapping : {})) {
28300
+ out[from.toLowerCase()] = to.toLowerCase();
28301
+ }
28302
+ return out;
28303
+ }
26679
28304
 
26680
28305
  /** Directus instance holding Kasu's pool editorial content. */
26681
28306
  const DEFAULT_DIRECTUS_URL = 'https://kasu-finance.directus.app/';
@@ -26963,17 +28588,143 @@ function ceilToCents(n) {
26963
28588
  return snapped < n ? (k + 1) / CENTS : snapped;
26964
28589
  }
26965
28590
 
28591
+ const TX_HASH = /^0x[0-9a-fA-F]{64}$/;
28592
+ const ADDRESS = /^0x[0-9a-fA-F]{40}$/;
28593
+ /** `https://host[:port][/path]`, no query, fragment, credentials or space. */
28594
+ const HTTPS_BASE = /^https:\/\/[A-Za-z0-9.-]+(:[0-9]{1,5})?(\/[A-Za-z0-9._~\-/]*)?$/;
28595
+ function explorerBase(config) {
28596
+ // Read defensively: a custom config written before 2.10.0 has no field,
28597
+ // and TypeScript only tells its author once they bump.
28598
+ const base = config === null || config === void 0 ? void 0 : config.blockExplorerUrl;
28599
+ if (typeof base !== 'string' || !HTTPS_BASE.test(base)) {
28600
+ throw new Error('explorer url: the chain config has no valid https blockExplorerUrl');
28601
+ }
28602
+ return base.replace(/\/+$/, '');
28603
+ }
28604
+ /**
28605
+ * `<blockExplorerUrl>/tx/<txHash>`. Throws unless `txHash` is exactly `0x`
28606
+ * followed by 64 hex digits.
28607
+ */
28608
+ function explorerTxUrl(config, txHash) {
28609
+ if (typeof txHash !== 'string' || !TX_HASH.test(txHash)) {
28610
+ throw new Error('explorerTxUrl: not a 32-byte 0x-prefixed hex hash');
28611
+ }
28612
+ return `${explorerBase(config)}/tx/${txHash}`;
28613
+ }
28614
+ /**
28615
+ * `<blockExplorerUrl>/address/<address>`, the address as given. Throws unless
28616
+ * `address` is `0x` followed by 40 hex digits with a valid checksum when it
28617
+ * is mixed-case (all-lower and all-upper carry no checksum and are accepted).
28618
+ */
28619
+ function explorerAddressUrl(config, address) {
28620
+ if (typeof address !== 'string' ||
28621
+ !ADDRESS.test(address) ||
28622
+ !ethers.utils.isAddress(address)) {
28623
+ throw new Error('explorerAddressUrl: not a valid 0x-prefixed address');
28624
+ }
28625
+ return `${explorerBase(config)}/address/${address}`;
28626
+ }
28627
+
28628
+ /**
28629
+ * One epoch's nominal length in days. A lock is counted in epochs, and the
28630
+ * loan-agreement text states its term from the same 7-day epoch.
28631
+ */
28632
+ const DAYS_PER_EPOCH = 7;
28633
+ /**
28634
+ * The average calendar month, 365.25 / 12. A lock is counted in epochs, so a
28635
+ * "12-month" term is 52 epochs = 364 days, and only the average-month divisor
28636
+ * lands that on 12 (364 / 30.4375 = 11.96 → 12).
28637
+ */
28638
+ const DAYS_PER_MONTH = 30.4375;
28639
+ /**
28640
+ * The shortest lock stated in months. Below it a term reads in weeks or days,
28641
+ * so nothing may claim a month count for it — a 3-epoch lock is 21 days, which
28642
+ * the average-month divisor would otherwise round UP to one month.
28643
+ */
28644
+ const MIN_DAYS_FOR_MONTHS = 30;
28645
+ /**
28646
+ * `'0'`, empty or absent is the variable-rate sentinel, not a fixed term.
28647
+ * Every surface that tags a rate "variable" or "fixed" must derive the tag
28648
+ * through this, so no screen can hardcode a tag that contradicts the rate
28649
+ * beside it.
28650
+ */
28651
+ function isFixedTermConfigId(configId) {
28652
+ if (configId == null)
28653
+ return false;
28654
+ const trimmed = configId.trim();
28655
+ return trimmed !== '' && trimmed !== '0';
28656
+ }
28657
+ /**
28658
+ * The fixed-term configs this wallet may actually select on a tranche: a
28659
+ * config open to `Everyone`, or an `AllowlistedOnly` config whose allowlist
28660
+ * names the wallet. Anything else is dropped (hide-ineligible): the contract
28661
+ * refuses a deposit into an allowlisted config from a wallet that is not on
28662
+ * it, so offering one is offering a transaction that reverts.
28663
+ *
28664
+ * Address matching is case-insensitive. An allowlist entry whose
28665
+ * `isAllowlisted` is `false` is a wallet that was REMOVED — the subgraph keeps
28666
+ * the entity and flips the flag — so it does not count. An entry that carries
28667
+ * no flag at all counts by its presence, which is how every entry read before
28668
+ * 2.9.0 arrived (the query did not select the field).
28669
+ *
28670
+ * `address` `null` / `undefined` — no wallet yet — sees only the `Everyone`
28671
+ * configs. Order is the tranche's own.
28672
+ */
28673
+ function availableFixedTermConfigs(tranche, address) {
28674
+ if (!tranche)
28675
+ return [];
28676
+ const lower = address === null || address === void 0 ? void 0 : address.toLowerCase();
28677
+ return tranche.fixedTermConfig.filter((config) => config.fixedTermDepositStatus === 'Everyone' ||
28678
+ (lower != null &&
28679
+ lower !== '' &&
28680
+ config.fixedTermDepositAllowlist.some((entry) => entry.isAllowlisted !==
28681
+ false && entry.userId.toLowerCase() === lower)));
28682
+ }
28683
+ /**
28684
+ * A lock's length in days: `epochLockDuration × 7`.
28685
+ *
28686
+ * FAILS CLOSED: an unparseable, non-finite, zero or negative epoch count
28687
+ * returns `NaN`, never `0` — a zero-day lock is not a term, and a consumer
28688
+ * that rendered it would state one.
28689
+ *
28690
+ * @param epochLockDuration the config's `epochLockDuration`, a decimal string.
28691
+ */
28692
+ function lockDurationDays(epochLockDuration) {
28693
+ const epochs = parseFloat(epochLockDuration);
28694
+ if (!Number.isFinite(epochs) || epochs <= 0)
28695
+ return NaN;
28696
+ return epochs * DAYS_PER_EPOCH;
28697
+ }
28698
+ /**
28699
+ * A lock in WHOLE months, or `null` when the epoch count is not a positive
28700
+ * number or the lock is shorter than `MIN_DAYS_FOR_MONTHS`.
28701
+ *
28702
+ * Unlike a year-rounding formatter it never rolls up into years: a 104-epoch
28703
+ * lock is 24 months. A consumer that states years does that conversion in its
28704
+ * own words.
28705
+ */
28706
+ function lockDurationMonths(epochLockDuration) {
28707
+ const days = lockDurationDays(epochLockDuration);
28708
+ if (Number.isNaN(days) || days < MIN_DAYS_FOR_MONTHS)
28709
+ return null;
28710
+ const months = Math.round(days / DAYS_PER_MONTH);
28711
+ return months > 0 ? months : null;
28712
+ }
28713
+
26966
28714
  const APXIUM = {
26967
28715
  name: 'Apxium',
26968
28716
  url: 'https://www.apxium.com/',
28717
+ region: 'Australia',
26969
28718
  };
26970
28719
  const INVOICEMATE = {
26971
28720
  name: 'InvoiceMate',
26972
28721
  url: 'https://invoicemate.net/',
28722
+ region: 'Global',
26973
28723
  };
26974
28724
  const RIXON_CAPITAL = {
26975
28725
  name: 'Rixon Capital',
26976
28726
  url: 'https://rixon.capital/',
28727
+ region: 'Australia',
26977
28728
  };
26978
28729
  function poolNameOf(pool) {
26979
28730
  var _a, _b;
@@ -27005,122 +28756,22 @@ function getCreditOriginator(pool) {
27005
28756
  function getInstitutionalLender(originator) {
27006
28757
  return originator.name === APXIUM.name ? RIXON_CAPITAL : null;
27007
28758
  }
27008
-
27009
- /**
27010
- * Cycles and the clearing window — the protocol's weekly clock, as numbers.
27011
- *
27012
- * Lifted verbatim from kasu-ui's `features/portfolio/lib/settlement-window.ts`
27013
- * and `features/lending/lib/cycle-dates.ts`. The two formatters that live
27014
- * beside `deriveCycleDates` there (`formatCycleDate`, `formatCycleCloseUtc`)
27015
- * print words and stay in the applications; everything here is unix seconds in
27016
- * and unix seconds out.
27017
- *
27018
- * The clearing window is the fixed 48 hours immediately preceding an epoch
27019
- * end. Inside it, pending requests are being processed and cannot be modified,
27020
- * and the countdown runs to the epoch end. Outside it, the countdown runs to
27021
- * the next clearing-window start.
27022
- *
27023
- * The epoch end comes straight from the chain (`nextEpochStartTimestamp`, i.e.
27024
- * the SDK's `getNextEpochDate`) — the same value the protocol's own
27025
- * `getNextClearingPeriodDate` derives from — so the window always lines up
27026
- * with the real weekly schedule (Tue 06:00 → Thu 06:00 UTC on Base) instead of
27027
- * a projected subgraph timestamp that can drift off the grid.
27028
- */
27029
- /**
27030
- * Clearing-window length — a fixed 48h protocol constant. Exported so every
27031
- * consumer derives the window from the same number this module runs on.
27032
- */
27033
- const CLEARING_WINDOW_SECONDS = 48 * 60 * 60;
27034
- /**
27035
- * Weekly cadence — the epoch schedule is fixed weekly (Tue → Thu UTC on Base).
27036
- * Used only to roll a cycle forward when a request lands inside a window that
27037
- * has already closed.
27038
- */
27039
- const WEEK_SECONDS = 7 * 24 * 60 * 60;
27040
- /**
27041
- * Which phase of the weekly cycle `nowSeconds` falls in, and how long is left
27042
- * of it.
27043
- *
27044
- * `'unknown'` when no epoch boundary has been loaded yet, or when the one on
27045
- * hand is stale (it has already elapsed — the on-chain value refetches to the
27046
- * next boundary shortly after rollover). A caller must render its "no cycle
27047
- * loaded" state there, never a zeroed countdown.
27048
- */
27049
- function computeSettlementWindow({ nowSeconds, nextEpochStart, clearingWindowSeconds = CLEARING_WINDOW_SECONDS, }) {
27050
- // No epoch boundary loaded yet, or a stale one that already elapsed.
27051
- if (nextEpochStart <= 0 || nextEpochStart <= nowSeconds) {
27052
- return { phase: 'unknown' };
27053
- }
27054
- const clearingStart = nextEpochStart - clearingWindowSeconds;
27055
- if (nowSeconds < clearingStart) {
27056
- return {
27057
- phase: 'awaiting',
27058
- secondsUntilClearing: clearingStart - nowSeconds,
27059
- nextClearingStart: clearingStart,
27060
- };
27061
- }
27062
- // nowSeconds is in [clearingStart, nextEpochStart) — inside the window.
27063
- return {
27064
- phase: 'clearing',
27065
- secondsUntilEpochEnd: nextEpochStart - nowSeconds,
27066
- epochEnd: nextEpochStart,
27067
- };
27068
- }
27069
- /**
27070
- * The next cycle boundary strictly after `nowSeconds`, in unix seconds — the
27071
- * cycle close (`nextEpochStart − 48h`) while the window is still open, the
27072
- * epoch end once we are inside it.
27073
- *
27074
- * `undefined` when there is no boundary left to wait for: no epoch boundary
27075
- * loaded, or a cached one that has already elapsed — the same staleness rule
27076
- * `computeSettlementWindow` applies before it reports `'unknown'`.
27077
- *
27078
- * Split out of the state machine because some consumers need the INSTANT
27079
- * rather than the phase: one to flush the cycle-dependent caches when the
27080
- * clock crosses it, one to move a pre-commit screen's snapshot clock at the
27081
- * same moment.
27082
- */
27083
- function nextCycleBoundary(nextEpochStart, nowSeconds, clearingWindowSeconds = CLEARING_WINDOW_SECONDS) {
27084
- if (!nextEpochStart || nextEpochStart <= 0)
27085
- return undefined;
27086
- const clearingStart = nextEpochStart - clearingWindowSeconds;
27087
- if (nowSeconds < clearingStart)
27088
- return clearingStart;
27089
- if (nowSeconds < nextEpochStart)
27090
- return nextEpochStart;
27091
- return undefined;
27092
- }
27093
28759
  /**
27094
- * The cycle-close and outcome dates for a request submitted `now`.
27095
- *
27096
- * - The cycle "closes" (stops accepting requests, starts processing) at the
27097
- * start of the 48h clearing window, i.e. 48h before the epoch end.
27098
- * - Processing takes up to 48h, so the outcome is confirmed by the epoch end
27099
- * (close + 48h).
27100
- *
27101
- * Returns `null` when the epoch boundary is not available or is stale — a
27102
- * caller then omits the dates entirely (omit, don't stub).
27103
- *
27104
- * The common case is a request submitted OUTSIDE the clearing window: the
27105
- * close is `nextEpochStart − 48h` and the outcome is `nextEpochStart`. When
27106
- * the request lands INSIDE the current clearing window (that close is already
27107
- * in the past), it queues for the NEXT weekly cycle, so the close is advanced
27108
- * by whole weeks until it is in the future.
28760
+ * The strategy's region — `'Australia'` for the Apxium family, `'Global'` for
28761
+ * PayFi — derived from its credit originator, exactly as every surface that
28762
+ * states a region must derive it.
28763
+ *
28764
+ * LOAD-BEARING ON THE MONEY PATH: this is the `region` a lender's wallet signs
28765
+ * in the loan-agreement message (`buildLoanAgreementSignMessage`). The backend
28766
+ * rebuilds that message from whatever region the client sends and treats it
28767
+ * as display-only, so nothing but this function keeps the signed word equal to
28768
+ * the word on screen. Never state a region from a constant — that is how a
28769
+ * PayFi lender came to pick "Global · Junior" and sign "Australia".
28770
+ *
28771
+ * Replace the inference with a per-pool field (in the SDK) when one ships.
27109
28772
  */
27110
- function deriveCycleDates(nextEpochStart, nowSeconds) {
27111
- // No boundary loaded, or a stale one that already elapsed — the same
27112
- // staleness rule as `computeSettlementWindow`.
27113
- if (!nextEpochStart || nextEpochStart <= nowSeconds)
27114
- return null;
27115
- let close = nextEpochStart - CLEARING_WINDOW_SECONDS;
27116
- let outcome = nextEpochStart;
27117
- // Inside the current clearing window the close already passed; a request
27118
- // now is queued for the next weekly cycle.
27119
- while (close <= nowSeconds) {
27120
- close += WEEK_SECONDS;
27121
- outcome += WEEK_SECONDS;
27122
- }
27123
- return { close, outcome };
28773
+ function getStrategyRegion(pool) {
28774
+ return getCreditOriginator(pool).region;
27124
28775
  }
27125
28776
 
27126
28777
  // Business rename (2026-07): Apxium strategies market their top retail tranche
@@ -27152,14 +28803,174 @@ function getTrancheDisplayName(trancheName, pool) {
27152
28803
  : trancheName;
27153
28804
  }
27154
28805
 
28806
+ /**
28807
+ * Forward-looking interest on a lender's CURRENT balances — the figures a
28808
+ * portfolio shows as "weekly", "daily" and the moving part of "lifetime"
28809
+ * interest, and the "≈ $Y a year" line beside a lend amount.
28810
+ *
28811
+ * Every figure is NET of the platform performance fee, the way the chain
28812
+ * credits it: `LendingPool._applyTrancheInterest` mints the lender
28813
+ * `interest × (1 − performanceFee)` at each cycle close, and only that net
28814
+ * amount ever reaches the balance. A gross projection would out-run what the
28815
+ * next close actually credits.
28816
+ *
28817
+ * Units: balances and results are display units of the deployment's stable
28818
+ * asset (not base units); rates are 0..1 fractions; `feePercent` is a
28819
+ * PERCENTAGE IN 0..100 — see `netEffectiveApy`.
28820
+ */
28821
+ /**
28822
+ * Seconds in one epoch's nominal week. The per-epoch rate covers exactly this
28823
+ * window, and the accrual spreads an epoch's interest across it.
28824
+ */
28825
+ const SECONDS_PER_WEEK = 604800;
28826
+ function isUsableFeePercent(feePercent) {
28827
+ return (feePercent !== null &&
28828
+ Number.isFinite(feePercent) &&
28829
+ feePercent >= 0 &&
28830
+ feePercent <= 100);
28831
+ }
28832
+ /**
28833
+ * Interest the slices accrue over ONE epoch, net of the fee.
28834
+ *
28835
+ * ```
28836
+ * weekly = Σ(slice.balance × slice.epochRate) × (1 − feePercent / 100)
28837
+ * ```
28838
+ *
28839
+ * **The rate is the raw per-epoch one, never the APY.** An APY is that rate
28840
+ * already compounded over `EPOCHS_IN_YEAR`; dividing it by 365 charges a day
28841
+ * of compounded annual growth to a single day and overstates it.
28842
+ *
28843
+ * FAILS CLOSED, returning `null` rather than a wrong number:
28844
+ * - a `null`, non-finite or out-of-domain fee — a fee-less projection dressed
28845
+ * up as the lender's net earnings is the failure this guards;
28846
+ * - any slice with a POSITIVE balance and a non-finite rate — skipping it
28847
+ * would return a silently partial total, quietly missing that slice.
28848
+ *
28849
+ * Non-finite and non-positive BALANCES are skipped: a slice with nothing in it
28850
+ * earns nothing whatever its rate, so it cannot hide earnings. A portfolio
28851
+ * with nothing lent is therefore a legitimate `0`, never `null`.
28852
+ */
28853
+ function weeklyInterest(slices, feePercent) {
28854
+ if (!isUsableFeePercent(feePercent))
28855
+ return null;
28856
+ let gross = 0;
28857
+ for (const slice of slices) {
28858
+ if (!Number.isFinite(slice.balance) || slice.balance <= 0)
28859
+ continue;
28860
+ if (!Number.isFinite(slice.epochRate))
28861
+ return null;
28862
+ gross += slice.balance * slice.epochRate;
28863
+ }
28864
+ return gross * (1 - feePercent / 100);
28865
+ }
28866
+ /**
28867
+ * A day's share of the epoch's net interest: a seventh of it, NOT a
28868
+ * de-compounded daily rate. The chain mints interest once per epoch, so
28869
+ * nothing compounds within the cycle and the accrual is linear across it.
28870
+ *
28871
+ * Fails closed with its input: `null` or non-finite → `null`.
28872
+ */
28873
+ function dailyInterest(weekly) {
28874
+ if (weekly === null || !Number.isFinite(weekly))
28875
+ return null;
28876
+ return weekly / 7;
28877
+ }
28878
+ /**
28879
+ * Interest accrued since each pool's OWN last cycle close — the moving part
28880
+ * of lifetime interest (lifetime = settled on chain + this).
28881
+ *
28882
+ * ```
28883
+ * accrued = Σ_pool weekly_pool × clamp(now − clearing_pool, 0, 604 800) / 604 800
28884
+ * ```
28885
+ *
28886
+ * **Per pool, because pools clear independently.** One shared anchor accrues
28887
+ * a stale pool's share from a close it never had, and when another pool
28888
+ * clears it drops the total by the stale pool's whole weekly share. Anchored
28889
+ * per pool, the total is monotonic across any single pool's clearing: that
28890
+ * pool swaps its accrued share for a capitalised one, and no other term moves.
28891
+ *
28892
+ * **The one-epoch cap is load-bearing.** Clearings are not exactly weekly,
28893
+ * and `weeklyInterest` is per EPOCH, so extrapolating past seven days would
28894
+ * promise interest the close will not credit; a late clearing under-reports
28895
+ * instead. The low clamp makes a close ahead of the local clock zero, never
28896
+ * negative.
28897
+ *
28898
+ * A non-finite term contributes nothing; a non-finite `nowSeconds` accrues
28899
+ * nothing. Callers fail closed upstream by not offering an accrual for a pool
28900
+ * whose weekly interest or anchor is unknown.
28901
+ */
28902
+ function accruedInterest(accruals, nowSeconds) {
28903
+ if (!Number.isFinite(nowSeconds))
28904
+ return 0;
28905
+ let accrued = 0;
28906
+ for (const { weeklyInterest: weekly, clearingTimestamp } of accruals) {
28907
+ if (!Number.isFinite(weekly) || !Number.isFinite(clearingTimestamp)) {
28908
+ continue;
28909
+ }
28910
+ const elapsed = Math.min(Math.max(nowSeconds - clearingTimestamp, 0), SECONDS_PER_WEEK);
28911
+ accrued += (weekly * elapsed) / SECONDS_PER_WEEK;
28912
+ }
28913
+ return accrued;
28914
+ }
28915
+ /**
28916
+ * What `amount` earns in a year at a NET rate — the "≈ $Y a year" line.
28917
+ * Feed it `netEffectiveApy(...)`, never a gross APY: the result is a lender's
28918
+ * earnings, so the fee must already be off the rate.
28919
+ *
28920
+ * FAILS CLOSED: a non-finite or negative amount or rate returns `NaN`, which
28921
+ * the consumer renders as "no figure" — exactly what `netEffectiveApy`
28922
+ * returns when the fee is unknown, so that state carries straight through.
28923
+ *
28924
+ * @param amount display units of the stable asset.
28925
+ * @param netApy the net effective annual rate as a 0..1 fraction.
28926
+ */
28927
+ function projectedAnnualEarnings(amount, netApy) {
28928
+ if (!Number.isFinite(amount) || amount < 0)
28929
+ return NaN;
28930
+ if (!Number.isFinite(netApy) || netApy < 0)
28931
+ return NaN;
28932
+ return amount * netApy;
28933
+ }
28934
+ /**
28935
+ * Balance-weighted average APY across slices, skipping non-finite and
28936
+ * non-positive balances and non-finite rates; `0` when nothing carries a
28937
+ * positive balance. Whatever basis goes in comes out — feed it NET rates for
28938
+ * an average effective (net) rate.
28939
+ */
28940
+ function weightedAverageApy(slices) {
28941
+ let totalBalance = 0;
28942
+ let weighted = 0;
28943
+ for (const slice of slices) {
28944
+ if (!Number.isFinite(slice.balance) || !Number.isFinite(slice.apy)) {
28945
+ continue;
28946
+ }
28947
+ if (slice.balance <= 0)
28948
+ continue;
28949
+ totalBalance += slice.balance;
28950
+ weighted += slice.balance * slice.apy;
28951
+ }
28952
+ if (totalBalance <= 0)
28953
+ return 0;
28954
+ return weighted / totalBalance;
28955
+ }
28956
+
27155
28957
  exports.APXIUM = APXIUM;
27156
28958
  exports.AU_ALPHA3 = AU_ALPHA3;
27157
28959
  exports.AU_MIN_CUMULATIVE_BY_STABLE = AU_MIN_CUMULATIVE_BY_STABLE;
28960
+ exports.BatchPendingError = BatchPendingError;
28961
+ exports.BatchRevertedError = BatchRevertedError;
28962
+ exports.BatchSubmissionError = BatchSubmissionError;
27158
28963
  exports.CHAIN_CONFIGS = CHAIN_CONFIGS;
27159
28964
  exports.CLEARING_WINDOW_SECONDS = CLEARING_WINDOW_SECONDS;
27160
28965
  exports.CONTRACT_TTL_MS = CONTRACT_TTL_MS;
28966
+ exports.CallsFacade = CallsFacade;
28967
+ exports.DAYS_PER_EPOCH = DAYS_PER_EPOCH;
28968
+ exports.DAYS_PER_MONTH = DAYS_PER_MONTH;
28969
+ exports.DEFAULT_BATCH_POLLING_POLICY = DEFAULT_BATCH_POLLING_POLICY;
28970
+ exports.DepositBatchMismatchError = DepositBatchMismatchError;
27161
28971
  exports.DepositFlow = DepositFlow;
27162
28972
  exports.DepositsFacade = DepositsFacade;
28973
+ exports.ENTRY_POINT_V07 = ENTRY_POINT_V07;
27163
28974
  exports.EPOCHS_IN_YEAR = EPOCHS_IN_YEAR;
27164
28975
  exports.Flow = Flow;
27165
28976
  exports.FlowStore = FlowStore;
@@ -27170,37 +28981,56 @@ exports.INVOICEMATE = INVOICEMATE;
27170
28981
  exports.Kasu = Kasu;
27171
28982
  exports.KasuSdk = KasuSdk;
27172
28983
  exports.MAX_LENDING_AMOUNT_FALLBACK = MAX_LENDING_AMOUNT_FALLBACK;
28984
+ exports.MIN_DAYS_FOR_MONTHS = MIN_DAYS_FOR_MONTHS;
27173
28985
  exports.MIN_LENDING_AMOUNT_FALLBACK = MIN_LENDING_AMOUNT_FALLBACK;
27174
28986
  exports.MIN_TRANCHE_CAPACITY = MIN_TRANCHE_CAPACITY;
28987
+ exports.NOT_A_WITHDRAWAL_CALL_MESSAGE = NOT_A_WITHDRAWAL_CALL_MESSAGE;
27175
28988
  exports.NO_DIRECTUS_URL_MESSAGE = NO_DIRECTUS_URL_MESSAGE;
27176
28989
  exports.NO_KYC_PARAMS_MESSAGE = NO_KYC_PARAMS_MESSAGE;
27177
28990
  exports.NO_SPENDER_MESSAGE = NO_SPENDER_MESSAGE;
28991
+ exports.NO_WITHDRAWAL_ENCODER_MESSAGE = NO_WITHDRAWAL_ENCODER_MESSAGE;
27178
28992
  exports.PortfolioFacade = PortfolioFacade;
27179
28993
  exports.READ_ONLY_MESSAGE = READ_ONLY_MESSAGE;
27180
28994
  exports.RIXON_CAPITAL = RIXON_CAPITAL;
28995
+ exports.SECONDS_PER_WEEK = SECONDS_PER_WEEK;
28996
+ exports.SPONSORED_CALL_KINDS = SPONSORED_CALL_KINDS;
28997
+ exports.SPONSORED_CALL_SELECTORS = SPONSORED_CALL_SELECTORS;
27181
28998
  exports.SdkConfig = SdkConfig;
28999
+ exports.SponsoredCallInputError = SponsoredCallInputError;
27182
29000
  exports.StrategiesFacade = StrategiesFacade;
27183
29001
  exports.UPPER_MEZZANINE = UPPER_MEZZANINE;
27184
29002
  exports.WithdrawFlow = WithdrawFlow;
29003
+ exports.accruedInterest = accruedInterest;
27185
29004
  exports.apyToEpochRate = apyToEpochRate;
27186
29005
  exports.asContractType = asContractType;
27187
29006
  exports.auMinimumRemaining = auMinimumRemaining;
27188
29007
  exports.auThresholdFor = auThresholdFor;
29008
+ exports.availableFixedTermConfigs = availableFixedTermConfigs;
29009
+ exports.batchOutcomeOf = batchOutcomeOf;
27189
29010
  exports.buildContractVersionType = buildContractVersionType;
27190
29011
  exports.buildFullNameRequestMessage = buildFullNameRequestMessage;
27191
29012
  exports.buildLegacyContractRequestMessage = buildLegacyContractRequestMessage;
27192
29013
  exports.buildLoanAgreementSignMessage = buildLoanAgreementSignMessage;
29014
+ exports.cancelDeadline = cancelDeadline;
27193
29015
  exports.ceilToCents = ceilToCents;
29016
+ exports.checkBatchOutcome = checkBatchOutcome;
27194
29017
  exports.classifyWalletFailure = classifyWalletFailure;
27195
29018
  exports.compareTrancheSeniority = compareTrancheSeniority;
27196
29019
  exports.computeSettlementWindow = computeSettlementWindow;
27197
29020
  exports.countSubmissions = countSubmissions;
29021
+ exports.dailyInterest = dailyInterest;
27198
29022
  exports.decodeRevert = decodeRevert;
27199
29023
  exports.deriveCycleDates = deriveCycleDates;
27200
29024
  exports.derivePoolStatus = derivePoolStatus;
27201
29025
  exports.deriveRequestState = deriveRequestState;
29026
+ exports.encodeCancelDepositCall = encodeCancelDepositCall;
29027
+ exports.encodeCancelWithdrawalCall = encodeCancelWithdrawalCall;
27202
29028
  exports.encodeDepositData = encodeDepositData;
29029
+ exports.encodeRequestWithdrawalCall = encodeRequestWithdrawalCall;
29030
+ exports.encodeWithdrawAtExpiryCall = encodeWithdrawAtExpiryCall;
27203
29031
  exports.epochRateToApy = epochRateToApy;
29032
+ exports.explorerAddressUrl = explorerAddressUrl;
29033
+ exports.explorerTxUrl = explorerTxUrl;
27204
29034
  exports.extractRevertData = extractRevertData;
27205
29035
  exports.fetchUnusedPoolIds = fetchUnusedPoolIds;
27206
29036
  exports.firstSubmissionTimestamp = firstSubmissionTimestamp;
@@ -27208,14 +29038,19 @@ exports.floorToCents = floorToCents;
27208
29038
  exports.formatSignTimestampUtc = formatSignTimestampUtc;
27209
29039
  exports.getCreditOriginator = getCreditOriginator;
27210
29040
  exports.getInstitutionalLender = getInstitutionalLender;
29041
+ exports.getStrategyRegion = getStrategyRegion;
27211
29042
  exports.getTrancheDisplayName = getTrancheDisplayName;
27212
29043
  exports.isAuMinimumExempt = isAuMinimumExempt;
27213
29044
  exports.isAustralianKyc = isAustralianKyc;
27214
29045
  exports.isBelowMinimumCapacity = isBelowMinimumCapacity;
27215
29046
  exports.isCycleClosed = isCycleClosed;
29047
+ exports.isDefinitivelyNotSubmitted = isDefinitivelyNotSubmitted;
29048
+ exports.isFixedTermConfigId = isFixedTermConfigId;
27216
29049
  exports.isUnpredictableGas = isUnpredictableGas;
27217
29050
  exports.isUserRejected = isUserRejected;
27218
29051
  exports.lastEventTimestamp = lastEventTimestamp;
29052
+ exports.lockDurationDays = lockDurationDays;
29053
+ exports.lockDurationMonths = lockDurationMonths;
27219
29054
  exports.maxNetRateCeiling = maxNetRateCeiling;
27220
29055
  exports.netEffectiveApy = netEffectiveApy;
27221
29056
  exports.netTrancheApyBounds = netTrancheApyBounds;
@@ -27227,10 +29062,18 @@ exports.pickDefaultTrancheId = pickDefaultTrancheId;
27227
29062
  exports.pickHighestYieldTranche = pickHighestYieldTranche;
27228
29063
  exports.poolAllTranchesFull = poolAllTranchesFull;
27229
29064
  exports.poolMaxApy = poolMaxApy;
29065
+ exports.projectedAnnualEarnings = projectedAnnualEarnings;
29066
+ exports.proveDepositInReceipt = proveDepositInReceipt;
29067
+ exports.proveUserOperationInReceipt = proveUserOperationInReceipt;
27230
29068
  exports.resolveBoundShortcuts = resolveBoundShortcuts;
27231
29069
  exports.resolveDepositBounds = resolveDepositBounds;
27232
29070
  exports.selectVisiblePools = selectVisiblePools;
27233
29071
  exports.submissionEvents = submissionEvents;
29072
+ exports.submitBatch = submitBatch;
29073
+ exports.submitSponsoredCall = submitSponsoredCall;
27234
29074
  exports.trancheApyBounds = trancheApyBounds;
27235
29075
  exports.trancheHasCapacity = trancheHasCapacity;
27236
29076
  exports.trancheRiskRank = trancheRiskRank;
29077
+ exports.validateDepositBatchCalls = validateDepositBatchCalls;
29078
+ exports.weeklyInterest = weeklyInterest;
29079
+ exports.weightedAverageApy = weightedAverageApy;