@kasufinance/kasu-sdk 2.7.0 → 2.8.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.
@@ -20837,12 +20837,12 @@ function toURLEncodedForm(data, options) {
20837
20837
  });
20838
20838
  }
20839
20839
 
20840
- const MAX_DEPTH = DEFAULT_FORM_DATA_MAX_DEPTH;
20840
+ const MAX_DEPTH$1 = DEFAULT_FORM_DATA_MAX_DEPTH;
20841
20841
 
20842
20842
  function throwIfDepthExceeded(index) {
20843
- if (index > MAX_DEPTH) {
20843
+ if (index > MAX_DEPTH$1) {
20844
20844
  throw new AxiosError$1(
20845
- 'FormData field is too deeply nested (' + index + ' levels). Max depth: ' + MAX_DEPTH,
20845
+ 'FormData field is too deeply nested (' + index + ' levels). Max depth: ' + MAX_DEPTH$1,
20846
20846
  AxiosError$1.ERR_FORM_DATA_DEPTH_EXCEEDED
20847
20847
  );
20848
20848
  }
@@ -24806,6 +24806,193 @@ function buildFullNameRequestMessage(address, timestampMs) {
24806
24806
  return `I request my full name for ${address.toLowerCase()} at ${timestampMs}.`;
24807
24807
  }
24808
24808
 
24809
+ // ---------------------------------------------------------------------------
24810
+ // The selector tables
24811
+ // ---------------------------------------------------------------------------
24812
+ /**
24813
+ * OpenZeppelin v5's ERC-20 errors. They are not in either Kasu ABI because no
24814
+ * Kasu contract declares them — they arrive from the TOKEN, through the
24815
+ * manager's `transferFrom`, and they are the modern spelling of the string
24816
+ * reverts below.
24817
+ */
24818
+ const ERC20_ERROR_ABI = [
24819
+ 'error ERC20InsufficientBalance(address sender, uint256 balance, uint256 needed)',
24820
+ 'error ERC20InsufficientAllowance(address spender, uint256 allowance, uint256 needed)',
24821
+ ];
24822
+ /**
24823
+ * The protocol errors that mean the ERC-20 transfer failed rather than the
24824
+ * protocol refusing.
24825
+ *
24826
+ * `SafeERC20FailedOperation` is OpenZeppelin's wrapper around a token call
24827
+ * that returned false or reverted, which on this path IS the `transferFrom`;
24828
+ * `AddressInsufficientBalance` is its native-value sibling. Both belong with
24829
+ * the balance case, not with the pool conditions — a lender who sees one of
24830
+ * them really does need to fund or re-approve.
24831
+ */
24832
+ const ERC20_FAMILY_NAMES = new Set([
24833
+ 'AddressInsufficientBalance',
24834
+ 'ERC20InsufficientAllowance',
24835
+ 'ERC20InsufficientBalance',
24836
+ 'SafeERC20FailedOperation',
24837
+ ]);
24838
+ /** Solidity's `Error(string)`, the selector every `require` reason arrives under. */
24839
+ const STRING_REVERT_SELECTOR = '0x08c379a0';
24840
+ /**
24841
+ * The `require` reasons that are a balance or an allowance, across the token
24842
+ * implementations in circulation: OpenZeppelin v4's sentences, and the
24843
+ * shorthand the minimal implementations use.
24844
+ */
24845
+ const ERC20_REASON_PATTERNS = [
24846
+ /exceeds\s+balance/,
24847
+ /exceeds\s+allowance/,
24848
+ /insufficient\s+balance/,
24849
+ /insufficient\s+allowance/,
24850
+ /transfer_from_failed/,
24851
+ /transferfrom\s+failed/,
24852
+ /low-level\s+call\s+failed/,
24853
+ ];
24854
+ /**
24855
+ * selector → error name, built once on first use.
24856
+ *
24857
+ * Lazily, because a consumer that never sees a revert should not pay for three
24858
+ * `Interface` constructions at import. The two Kasu tables are the GENERATED
24859
+ * typechain ABIs, so this cannot drift from the contracts: regenerate the ABIs
24860
+ * and the new errors are recognised with no edit here.
24861
+ */
24862
+ let selectorTable = null;
24863
+ function selectors() {
24864
+ if (selectorTable)
24865
+ return selectorTable;
24866
+ const table = new Map();
24867
+ const interfaces = [
24868
+ // The manager the deposit is called against, and the allow list it
24869
+ // delegates the KYC and block-window checks to.
24870
+ ILendingPoolManagerAbi__factory.createInterface(),
24871
+ IKasuAllowListAbi__factory.createInterface(),
24872
+ new ethers.ethers.utils.Interface(ERC20_ERROR_ABI),
24873
+ ];
24874
+ for (const iface of interfaces) {
24875
+ for (const fragment of Object.values(iface.errors)) {
24876
+ // First writer wins: the same name declared by both ABIs is the
24877
+ // same selector, so the duplicate would be a no-op anyway.
24878
+ const sighash = iface.getSighash(fragment);
24879
+ if (!table.has(sighash))
24880
+ table.set(sighash, fragment.name);
24881
+ }
24882
+ }
24883
+ selectorTable = table;
24884
+ return table;
24885
+ }
24886
+ // ---------------------------------------------------------------------------
24887
+ // Getting the bytes out of the envelope
24888
+ // ---------------------------------------------------------------------------
24889
+ /**
24890
+ * How deep to look for the revert data. Providers nest it differently —
24891
+ * ethers' own error wraps the provider's, which wraps the RPC body — and a
24892
+ * bound is what keeps a self-referencing error object from being walked
24893
+ * forever.
24894
+ */
24895
+ const MAX_DEPTH = 6;
24896
+ /** The keys ethers and the providers under it put a wrapped error on. */
24897
+ const NESTED_KEYS = [
24898
+ 'data',
24899
+ 'error',
24900
+ 'originalError',
24901
+ 'info',
24902
+ 'cause',
24903
+ 'body',
24904
+ ];
24905
+ /** At least a 4-byte selector, and a whole number of bytes. */
24906
+ function isRevertData(value) {
24907
+ return (value.length >= 10 &&
24908
+ value.length % 2 === 0 &&
24909
+ /^0x[0-9a-fA-F]+$/.test(value));
24910
+ }
24911
+ /**
24912
+ * Find the revert data on an error, wherever the provider left it.
24913
+ *
24914
+ * ethers v5 raises `UNPREDICTABLE_GAS_LIMIT` with the provider's own error
24915
+ * attached, and each provider nests the bytes one layer differently:
24916
+ * `error.data`, `error.error.data`, `error.data.originalError.data`, or only
24917
+ * inside the JSON `body` of a `SERVER_ERROR`. Rather than enumerate the
24918
+ * products, walk a fixed set of keys to a fixed depth and take the first value
24919
+ * that is shaped like revert data.
24920
+ */
24921
+ function extractRevertData(err) {
24922
+ return walk(err, 0);
24923
+ }
24924
+ function walk(node, depth) {
24925
+ if (node == null || depth > MAX_DEPTH)
24926
+ return null;
24927
+ if (typeof node === 'string') {
24928
+ if (isRevertData(node))
24929
+ return node;
24930
+ // A `SERVER_ERROR`'s body: the RPC's JSON reply, verbatim.
24931
+ if (!node.startsWith('{'))
24932
+ return null;
24933
+ try {
24934
+ return walk(JSON.parse(node), depth + 1);
24935
+ }
24936
+ catch (_a) {
24937
+ return null;
24938
+ }
24939
+ }
24940
+ if (typeof node !== 'object')
24941
+ return null;
24942
+ const record = node;
24943
+ for (const key of NESTED_KEYS) {
24944
+ const found = walk(record[key], depth + 1);
24945
+ if (found)
24946
+ return found;
24947
+ }
24948
+ return null;
24949
+ }
24950
+ // ---------------------------------------------------------------------------
24951
+ // Decoding
24952
+ // ---------------------------------------------------------------------------
24953
+ /**
24954
+ * Decode a failed call's revert into the error the contract named, or `null`
24955
+ * when there is nothing recognisable to name.
24956
+ *
24957
+ * `null` is the honest answer for an unknown selector, an unrelated `require`
24958
+ * string, or an error with no revert data at all — the caller keeps whatever
24959
+ * it did before rather than inventing a diagnosis from bytes it cannot read.
24960
+ */
24961
+ function decodeRevert(err) {
24962
+ const data = extractRevertData(err);
24963
+ if (!data)
24964
+ return null;
24965
+ const selector = data.slice(0, 10).toLowerCase();
24966
+ if (selector === STRING_REVERT_SELECTOR) {
24967
+ const reason = decodeStringRevert(data);
24968
+ if (reason === null)
24969
+ return null;
24970
+ return isErc20Reason(reason)
24971
+ ? { name: 'Error', family: 'erc20', reason }
24972
+ : null;
24973
+ }
24974
+ const name = selectors().get(selector);
24975
+ if (!name)
24976
+ return null;
24977
+ return {
24978
+ name,
24979
+ family: ERC20_FAMILY_NAMES.has(name) ? 'erc20' : 'protocol',
24980
+ };
24981
+ }
24982
+ function decodeStringRevert(data) {
24983
+ try {
24984
+ const [reason] = ethers.ethers.utils.defaultAbiCoder.decode(['string'], `0x${data.slice(10)}`);
24985
+ return reason;
24986
+ }
24987
+ catch (_a) {
24988
+ return null;
24989
+ }
24990
+ }
24991
+ function isErc20Reason(reason) {
24992
+ const lower = reason.toLowerCase();
24993
+ return ERC20_REASON_PATTERNS.some((pattern) => pattern.test(lower));
24994
+ }
24995
+
24809
24996
  /**
24810
24997
  * Wallet and RPC error predicates — pure functions over `unknown`.
24811
24998
  *
@@ -24961,19 +25148,94 @@ function classifyWalletFailure(step, err) {
24961
25148
  : { step, reason: 'failed', error: err };
24962
25149
  }
24963
25150
 
25151
+ /**
25152
+ * What a `depositBatch` port throws when it KNOWS the batch reverted — from a
25153
+ * simulation before sending, or from the submitter refusing to broadcast a
25154
+ * batch that would revert. The run ends `{ step: 'request', reason:
25155
+ * 'reverted', revertError }`.
25156
+ *
25157
+ * `revertError` is the custom error the contract named (`'ClearingIsPending'`,
25158
+ * `'UserNotKycd'`, …), exactly as the ABI declares it. Pass `''` when the
25159
+ * batch is known to revert but not why: the consumer then renders its generic
25160
+ * `reverted` copy, which is what an unknown name gets anyway.
25161
+ *
25162
+ * Any OTHER throw from the port is read the way a direct deposit's is — a
25163
+ * wallet rejection is `cancelled`, a decodable revert is `reverted`, and
25164
+ * everything else is `failed` with the error kept. A port that cannot tell is
25165
+ * better off throwing what it has than guessing a name.
25166
+ */
25167
+ class BatchRevertedError extends Error {
25168
+ constructor(revertError, message) {
25169
+ super(message !== null && message !== void 0 ? message : `DepositFlow: the batch reverted${revertError ? ` with ${revertError}` : ''}`);
25170
+ this.name = 'BatchRevertedError';
25171
+ this.revertError = revertError;
25172
+ // Keeps `instanceof` honest under a down-levelled build.
25173
+ Object.setPrototypeOf(this, BatchRevertedError.prototype);
25174
+ }
25175
+ }
24964
25176
  /** Generated agreements are valid for five minutes upstream. */
24965
25177
  const CONTRACT_TTL_MS = 5 * 60 * 1000;
24966
25178
  /** What a run without a spender, from either source, fails with. */
24967
25179
  const NO_SPENDER_MESSAGE = 'DepositFlow: no ERC-20 spender; build the flow with kasu.flows.deposit() or pass `spender` on the input';
25180
+ /** What a malformed auth signature fails the generate step with. */
25181
+ const INVALID_AUTH_SIGNATURE_MESSAGE = 'DepositFlow: the wallet returned a malformed authentication signature; expected 65 bytes of 0x-prefixed hex';
25182
+ /** What a malformed acceptance signature fails the confirm step with. */
25183
+ const INVALID_ACCEPTANCE_SIGNATURE_MESSAGE = 'DepositFlow: the wallet returned a malformed acceptance signature; expected 65 bytes of 0x-prefixed hex';
25184
+ /**
25185
+ * Is this an EIP-191 personal sign, in the only shape the protocol accepts?
25186
+ *
25187
+ * 65 bytes — `r`, `s`, `v` — 0x-prefixed. Nothing downstream checks it: the
25188
+ * ABI coder takes any even-length hex as `bytes`, so `'0x'` and a 64-byte
25189
+ * string both encode happily into a `depositData` the agreements service then
25190
+ * cannot verify against anything. The check has to happen where the signature
25191
+ * is taken, which is here.
25192
+ */
25193
+ function isPersonalSignature(signature) {
25194
+ return ethers.utils.isHexString(signature, 65);
25195
+ }
24968
25196
  const INITIAL$1 = {
24969
25197
  phase: 'idle',
24970
25198
  step: null,
24971
25199
  stepIndex: 0,
24972
25200
  stepTotal: 4,
24973
25201
  approvalRequired: true,
25202
+ batched: false,
24974
25203
  contract: null,
24975
25204
  failure: null,
24976
25205
  };
25206
+ const ERC20 = IERC20MetadataAbi__factory.createInterface();
25207
+ const LENDING_POOL_MANAGER = ILendingPoolManagerAbi__factory.createInterface();
25208
+ /**
25209
+ * The batch path's two calls, encoded by the SDK so no consumer has to: the
25210
+ * EXACT-amount approve, then the very `requestDepositWithKyc` the default
25211
+ * `deposit` port sends — `swapData` `'0x'`, no value. Both read `amount` from
25212
+ * the same variable, so the batch cannot approve one figure and deposit
25213
+ * another.
25214
+ */
25215
+ function encodeDepositBatch(args) {
25216
+ const { stableAsset, spender, input, depositData, kyc } = args;
25217
+ return [
25218
+ {
25219
+ to: stableAsset,
25220
+ data: ERC20.encodeFunctionData('approve', [spender, input.amount]),
25221
+ },
25222
+ {
25223
+ to: spender,
25224
+ data: LENDING_POOL_MANAGER.encodeFunctionData('requestDepositWithKyc', [
25225
+ input.poolId,
25226
+ input.trancheId,
25227
+ input.amount,
25228
+ '0x',
25229
+ input.fixedTermConfigId,
25230
+ depositData,
25231
+ {
25232
+ blockExpiration: kyc.blockExpiration,
25233
+ signature: kyc.signature,
25234
+ },
25235
+ ]),
25236
+ },
25237
+ ];
25238
+ }
24977
25239
  /** 1-based badge position, with `approve` dropped when it is out of scope. */
24978
25240
  function stepIndexOf(step, approvalRequired) {
24979
25241
  const order = approvalRequired
@@ -24989,17 +25251,52 @@ function badgeFor(step, approvalRequired) {
24989
25251
  };
24990
25252
  }
24991
25253
  /**
24992
- * The request step has a third outcome. `UNPREDICTABLE_GAS_LIMIT` here is
24993
- * almost always `transferFrom` reverting on a balance that cannot cover the
24994
- * deposit, and it takes precedence: nothing was refused by the lender, so
24995
- * inviting a retry would just reproduce it.
25254
+ * The request step's extra outcomes, in the order the evidence supports.
25255
+ *
25256
+ * A revert is read for WHAT reverted before it is read as a shortfall.
25257
+ * `requestDepositWithKyc` reverts for a whole family of declared reasons —
25258
+ * `LendingPoolIsStopped`, `ClearingIsPending`, `UserNotKycd`, `UserBlocked`,
25259
+ * `UserNotInAllowList`, `InvalidTranche`, `BlockExpired` — and every one of
25260
+ * them used to arrive as `insufficient-balance`, which told a fully funded
25261
+ * lender to top up a wallet that was never short while their pool was simply
25262
+ * mid-clearing. So: a decoded protocol error is `reverted` and names itself.
25263
+ *
25264
+ * Only a token balance or allowance failure — or revert data nothing can
25265
+ * decode, on the `UNPREDICTABLE_GAS_LIMIT` ethers raises for a failed gas
25266
+ * estimate — is still `insufficient-balance`, which is the case that reason
25267
+ * was named for and the one where topping up is genuinely the answer.
24996
25268
  */
24997
25269
  function classifyRequest(err) {
24998
- if (isUnpredictableGas(err)) {
25270
+ const revert = decodeRevert(err);
25271
+ if ((revert === null || revert === void 0 ? void 0 : revert.family) === 'protocol') {
25272
+ return {
25273
+ step: 'request',
25274
+ reason: 'reverted',
25275
+ revertError: revert.name,
25276
+ error: err,
25277
+ };
25278
+ }
25279
+ if (revert !== null || isUnpredictableGas(err)) {
24999
25280
  return { step: 'request', reason: 'insufficient-balance', error: err };
25000
25281
  }
25001
25282
  return classifyWalletFailure('request', err);
25002
25283
  }
25284
+ /**
25285
+ * A `depositBatch` rejection. A submitter that KNOWS the batch reverts says so
25286
+ * with `BatchRevertedError`; anything else is read exactly as a direct
25287
+ * deposit's throw is, so a non-wallet error can never become `cancelled`.
25288
+ */
25289
+ function classifyBatch(err) {
25290
+ if (err instanceof BatchRevertedError) {
25291
+ return {
25292
+ step: 'request',
25293
+ reason: 'reverted',
25294
+ revertError: err.revertError,
25295
+ error: err,
25296
+ };
25297
+ }
25298
+ return classifyRequest(err);
25299
+ }
25003
25300
  // ---------------------------------------------------------------------------
25004
25301
  // The flow
25005
25302
  // ---------------------------------------------------------------------------
@@ -25026,8 +25323,15 @@ class DepositFlow extends Flow {
25026
25323
  * A stale token simply is not the current generation.
25027
25324
  */
25028
25325
  this._acceptingFor = null;
25326
+ /**
25327
+ * The batch path's authorisation handshake. Same shape and lifetime as
25328
+ * `_accept`: the run parks on it, `authoriseTransfer`, `declineTransfer`
25329
+ * and `reset` settle it, and it is cleared the moment it settles.
25330
+ */
25331
+ this._authorise = null;
25029
25332
  this._ttlMs = (_a = opts === null || opts === void 0 ? void 0 : opts.contractTtlMs) !== null && _a !== void 0 ? _a : CONTRACT_TTL_MS;
25030
25333
  this._defaultSpender = opts === null || opts === void 0 ? void 0 : opts.spender;
25334
+ this._stableAsset = opts === null || opts === void 0 ? void 0 : opts.stableAsset;
25031
25335
  // Called through the ports object, never captured off it: a consumer
25032
25336
  // whose clock is a method on its own object keeps its `this`.
25033
25337
  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(); };
@@ -25047,6 +25351,18 @@ class DepositFlow extends Flow {
25047
25351
  this._store.patch(Object.assign({ phase: 'accepting-sign' }, badgeFor('confirm', this._store.state.approvalRequired)), token);
25048
25352
  try {
25049
25353
  const signature = yield this._ports.signMessage(contract.contractMessage);
25354
+ // Checked HERE, not at the encoder: this is the signature the
25355
+ // agreements service verifies the deposit against, and a wallet
25356
+ // that returned something other than 65 bytes has not signed
25357
+ // anything. Failing the confirm step names the step that produced
25358
+ // it; letting it through would put an unverifiable blob on chain.
25359
+ if (!isPersonalSignature(signature)) {
25360
+ bridge.settle({
25361
+ kind: 'failed',
25362
+ error: new Error(INVALID_ACCEPTANCE_SIGNATURE_MESSAGE),
25363
+ });
25364
+ return;
25365
+ }
25050
25366
  bridge.settle({ kind: 'accepted', signature });
25051
25367
  }
25052
25368
  catch (err) {
@@ -25077,6 +25393,33 @@ class DepositFlow extends Flow {
25077
25393
  this._accept = null;
25078
25394
  bridge.settle({ kind: 'declined' });
25079
25395
  }
25396
+ /**
25397
+ * Batch path: authorise the single transaction that approves and deposits,
25398
+ * and resume the parked run. A no-op when nothing is parked on
25399
+ * `awaiting-authorise`, so a double tap cannot submit twice.
25400
+ *
25401
+ * The batch has no wallet prompt per call, so this is the lender's
25402
+ * explicit consent to the transfer — call it only from their action.
25403
+ */
25404
+ authoriseTransfer() {
25405
+ const bridge = this._authorise;
25406
+ if (!bridge)
25407
+ return;
25408
+ this._authorise = null;
25409
+ bridge.settle({ kind: 'authorised' });
25410
+ }
25411
+ /**
25412
+ * Batch path: refuse the transfer. The run ends on `error` with `{ step:
25413
+ * 'request', reason: 'cancelled' }` and nothing is submitted. A no-op when
25414
+ * nothing is parked on `awaiting-authorise`.
25415
+ */
25416
+ declineTransfer() {
25417
+ const bridge = this._authorise;
25418
+ if (!bridge)
25419
+ return;
25420
+ this._authorise = null;
25421
+ bridge.settle({ kind: 'declined' });
25422
+ }
25080
25423
  /** True only while THIS generation is waiting on the acceptance signature. */
25081
25424
  _isAccepting() {
25082
25425
  return (this._acceptingFor !== null &&
@@ -25088,6 +25431,9 @@ class DepositFlow extends Flow {
25088
25431
  this._accept = null;
25089
25432
  this._acceptingFor = null;
25090
25433
  bridge === null || bridge === void 0 ? void 0 : bridge.settle({ kind: 'abandoned' });
25434
+ const authorise = this._authorise;
25435
+ this._authorise = null;
25436
+ authorise === null || authorise === void 0 ? void 0 : authorise.settle({ kind: 'abandoned' });
25091
25437
  }
25092
25438
  // -----------------------------------------------------------------------
25093
25439
  _run(input, token) {
@@ -25095,6 +25441,7 @@ class DepositFlow extends Flow {
25095
25441
  var _a;
25096
25442
  this._accept = null;
25097
25443
  this._acceptingFor = null;
25444
+ this._authorise = null;
25098
25445
  const ports = this._ports;
25099
25446
  const owner = input.userAddress.toLowerCase();
25100
25447
  // 0. The spender, from the input or from the chain config the facade
@@ -25115,21 +25462,32 @@ class DepositFlow extends Flow {
25115
25462
  // live, never cached: an exact-amount approval is fully consumed by
25116
25463
  // the deposit it paid for, so a stale allowance is exactly the value
25117
25464
  // that would wrongly skip the approve and revert the deposit.
25118
- let approvalRequired = true;
25465
+ let allowanceShort = true;
25119
25466
  try {
25120
25467
  const allowance = yield ports.readAllowance(owner, spender);
25121
- approvalRequired = allowance.lt(input.amount);
25468
+ allowanceShort = allowance.lt(input.amount);
25122
25469
  }
25123
25470
  catch (_b) {
25124
25471
  // A read failure is not a reason to skip an approval. Assume one is
25125
25472
  // needed: the cost is a redundant approve, the alternative is a
25126
25473
  // reverted deposit.
25127
- approvalRequired = true;
25474
+ allowanceShort = true;
25128
25475
  }
25129
25476
  if (!this._store.isCurrent(token))
25130
25477
  return;
25478
+ // The path is chosen HERE, with the allowance, so the badge is right
25479
+ // from the first state the lender sees. Batch only when there is an
25480
+ // approval to fold in, a port to submit it, and a known token to
25481
+ // approve on — never a guessed one. A batched run never enters the
25482
+ // `approve` step, so every badge counts it as a three-step run.
25483
+ const stableAsset = this._stableAsset;
25484
+ const batched = allowanceShort &&
25485
+ typeof ports.depositBatch === 'function' &&
25486
+ !!stableAsset;
25487
+ const approvalRequired = allowanceShort && !batched;
25131
25488
  // 2. Generate — sign the auth message.
25132
- this._store.patch(Object.assign({ approvalRequired, phase: 'generating-sign' }, badgeFor('generate', approvalRequired)), token);
25489
+ this._store.patch(Object.assign({ approvalRequired,
25490
+ batched, phase: 'generating-sign' }, badgeFor('generate', approvalRequired)), token);
25133
25491
  const timestamp = this._now();
25134
25492
  const signedMessage = buildAuthMessage(input, owner, timestamp);
25135
25493
  let signature;
@@ -25140,6 +25498,18 @@ class DepositFlow extends Flow {
25140
25498
  this._fail(token, approvalRequired, classifyWalletFailure('generate', err));
25141
25499
  return;
25142
25500
  }
25501
+ // The same EIP-191 shape as the acceptance, and the same reason to
25502
+ // check it: kasu-backend verifies this one to decide whether to issue
25503
+ // an agreement at all, so a malformed signature is a 4xx that says
25504
+ // nothing about which step produced it. Named here instead.
25505
+ if (!isPersonalSignature(signature)) {
25506
+ this._fail(token, approvalRequired, {
25507
+ step: 'generate',
25508
+ reason: 'failed',
25509
+ error: new Error(INVALID_AUTH_SIGNATURE_MESSAGE),
25510
+ });
25511
+ return;
25512
+ }
25143
25513
  if (!this._store.isCurrent(token))
25144
25514
  return;
25145
25515
  // 3. Generate — POST the request.
@@ -25196,12 +25566,41 @@ class DepositFlow extends Flow {
25196
25566
  });
25197
25567
  return;
25198
25568
  }
25199
- const depositData = encodeDepositData({
25200
- signature: outcome.signature,
25201
- timestamp: contract.timestamp,
25202
- contractVersion: contract.contractVersion,
25203
- contractType: asContractType(contract.contractType),
25204
- });
25569
+ // The acceptance signature is already known to be 65 bytes, but the
25570
+ // encoder also reads `timestamp`, `contractVersion` and
25571
+ // `contractType` off a response the SDK did not produce. `start()`
25572
+ // promises it does not reject, so anything that throws in here becomes
25573
+ // a failure the consumer can render rather than an unhandled rejection
25574
+ // in an application that was told it never had to catch one.
25575
+ let depositData;
25576
+ try {
25577
+ depositData = encodeDepositData({
25578
+ signature: outcome.signature,
25579
+ timestamp: contract.timestamp,
25580
+ contractVersion: contract.contractVersion,
25581
+ contractType: asContractType(contract.contractType),
25582
+ });
25583
+ }
25584
+ catch (err) {
25585
+ this._fail(token, approvalRequired, {
25586
+ step: 'request',
25587
+ reason: 'failed',
25588
+ error: err,
25589
+ });
25590
+ return;
25591
+ }
25592
+ if (batched && stableAsset) {
25593
+ yield this._runBatch({
25594
+ token,
25595
+ input,
25596
+ owner,
25597
+ spender,
25598
+ stableAsset,
25599
+ depositData,
25600
+ contract,
25601
+ });
25602
+ return;
25603
+ }
25205
25604
  // 6. Approve — the EXACT amount, never `MaxUint256`. House rule: an
25206
25605
  // unlimited allowance outlives the deposit it was granted for, and a
25207
25606
  // later exploit of the spender would drain a wallet that has long
@@ -25265,6 +25664,108 @@ class DepositFlow extends Flow {
25265
25664
  this._store.patch(Object.assign({ phase: 'success' }, badgeFor('request', approvalRequired)), token);
25266
25665
  });
25267
25666
  }
25667
+ /**
25668
+ * The batch path, from the accepted agreement to the receipt.
25669
+ *
25670
+ * 1. PARK on `awaiting-authorise`. A batch puts no wallet prompt in front
25671
+ * of each call, so the flow waits for an explicit authorisation before
25672
+ * any money moves. `declineTransfer()` ends it as `cancelled`.
25673
+ * 2. Re-check the TTL: the lender may have idled on the authorisation as
25674
+ * long as they did on the agreement.
25675
+ * 3. Only now fetch the KYC signature — it has a block expiration of its
25676
+ * own, and an authorisation left open must not spend it.
25677
+ * 4. Encode both calls and hand them to `depositBatch`; then the receipt.
25678
+ */
25679
+ _runBatch(args) {
25680
+ return __awaiter(this, void 0, void 0, function* () {
25681
+ const { token, input, owner, spender, stableAsset, depositData } = args;
25682
+ const ports = this._ports;
25683
+ // A batched run is a three-step run on every badge.
25684
+ const approvalRequired = false;
25685
+ // Every settler clears the handshake before settling it, so there is
25686
+ // nothing to clear here — and clearing it anyway could drop the
25687
+ // handshake of a NEWER run that `reset(); start()` already parked.
25688
+ const outcome = yield new Promise((resolve) => {
25689
+ this._authorise = { settle: resolve };
25690
+ this._store.patch(Object.assign({ phase: 'awaiting-authorise' }, badgeFor('request', approvalRequired)), token);
25691
+ });
25692
+ if (!this._store.isCurrent(token))
25693
+ return;
25694
+ if (outcome.kind === 'declined') {
25695
+ this._fail(token, approvalRequired, {
25696
+ step: 'request',
25697
+ reason: 'cancelled',
25698
+ });
25699
+ return;
25700
+ }
25701
+ if (outcome.kind !== 'authorised')
25702
+ return;
25703
+ if (this._now() > args.contract.timestamp + this._ttlMs) {
25704
+ this._fail(token, approvalRequired, {
25705
+ step: 'request',
25706
+ reason: 'contract-expired',
25707
+ });
25708
+ return;
25709
+ }
25710
+ this._store.patch(Object.assign({ phase: 'request-sign' }, badgeFor('request', approvalRequired)), token);
25711
+ // The consumer's backend, so `failed` — never `cancelled` — exactly as
25712
+ // on the two-transaction path.
25713
+ let kyc;
25714
+ try {
25715
+ const kycParams = yield ports.buildKycParams(owner);
25716
+ kyc = yield ports.getKycSignature(kycParams);
25717
+ }
25718
+ catch (err) {
25719
+ this._fail(token, approvalRequired, {
25720
+ step: 'request',
25721
+ reason: 'failed',
25722
+ error: err,
25723
+ });
25724
+ return;
25725
+ }
25726
+ if (!this._store.isCurrent(token))
25727
+ return;
25728
+ // Encoded from values a backend returned (the KYC signature), so a
25729
+ // shape the coder refuses is a failure the consumer can render, not a
25730
+ // rejection out of a `start()` that promises it has none.
25731
+ let calls;
25732
+ try {
25733
+ calls = encodeDepositBatch({
25734
+ stableAsset,
25735
+ spender,
25736
+ input,
25737
+ depositData,
25738
+ kyc,
25739
+ });
25740
+ }
25741
+ catch (err) {
25742
+ this._fail(token, approvalRequired, {
25743
+ step: 'request',
25744
+ reason: 'failed',
25745
+ error: err,
25746
+ });
25747
+ return;
25748
+ }
25749
+ try {
25750
+ // Called through the ports object, so a port that is a method
25751
+ // keeps its `this`. Its presence decided the path; a consumer
25752
+ // that removed it since gets a `failed`, not a crash.
25753
+ if (typeof ports.depositBatch !== 'function') {
25754
+ throw new Error('DepositFlow: the depositBatch port is gone');
25755
+ }
25756
+ const tx = yield ports.depositBatch(calls);
25757
+ this._store.patch(Object.assign({ phase: 'request-confirm' }, badgeFor('request', approvalRequired)), token);
25758
+ yield tx.wait();
25759
+ }
25760
+ catch (err) {
25761
+ this._fail(token, approvalRequired, classifyBatch(err));
25762
+ return;
25763
+ }
25764
+ if (!this._store.isCurrent(token))
25765
+ return;
25766
+ this._store.patch(Object.assign({ phase: 'success' }, badgeFor('request', approvalRequired)), token);
25767
+ });
25768
+ }
25268
25769
  _fail(token, approvalRequired, failure) {
25269
25770
  this._store.patch(Object.assign({ phase: 'error', failure }, badgeFor(failure.step, approvalRequired)), token);
25270
25771
  }
@@ -25417,11 +25918,18 @@ class FlowsFacade {
25417
25918
  * an address whose only wrong value grants an approval to the wrong
25418
25919
  * contract.
25419
25920
  */
25420
- _spender) {
25921
+ _spender,
25922
+ /**
25923
+ * This chain's stable token address — the target of the batch path's
25924
+ * approve. `undefined` for a hand-built config without `stableAsset`,
25925
+ * in which case a run never batches: it does not guess a token.
25926
+ */
25927
+ _stableAsset) {
25421
25928
  this._deposits = _deposits;
25422
25929
  this._erc20 = _erc20;
25423
25930
  this._isReadOnly = _isReadOnly;
25424
25931
  this._spender = _spender;
25932
+ this._stableAsset = _stableAsset;
25425
25933
  }
25426
25934
  /**
25427
25935
  * A deposit pipeline. `readAllowance`, `approve`, `deposit` and
@@ -25434,9 +25942,13 @@ class FlowsFacade {
25434
25942
  * delete the default it was meant to keep, and
25435
25943
  * `approve: sponsoredGas ? sponsoredApprove : undefined` is exactly how a
25436
25944
  * consumer writes a conditional override.
25945
+ *
25946
+ * `depositBatch` has NO default: how several calls become one atomic
25947
+ * transaction is the application's business. Supplying it lets a run
25948
+ * whose allowance is short approve and deposit in one batch.
25437
25949
  */
25438
25950
  deposit(ports, opts) {
25439
- var _a, _b, _c, _d, _e;
25951
+ var _a, _b, _c, _d, _e, _f;
25440
25952
  return new DepositFlow({
25441
25953
  signMessage: ports.signMessage,
25442
25954
  generateContract: ports.generateContract,
@@ -25451,12 +25963,14 @@ class FlowsFacade {
25451
25963
  return yield this._erc20().approve(spender, amount);
25452
25964
  })),
25453
25965
  deposit: (_d = ports.deposit) !== null && _d !== void 0 ? _d : ((params) => this._deposits.deposit(params)),
25966
+ depositBatch: ports.depositBatch,
25454
25967
  now: ports.now,
25455
25968
  }, {
25456
25969
  contractTtlMs: opts === null || opts === void 0 ? void 0 : opts.contractTtlMs,
25457
25970
  // Per key here too, for the same reason the ports are: an
25458
25971
  // explicit `spender: undefined` must not delete the default.
25459
25972
  spender: (_e = opts === null || opts === void 0 ? void 0 : opts.spender) !== null && _e !== void 0 ? _e : this._spender,
25973
+ stableAsset: (_f = opts === null || opts === void 0 ? void 0 : opts.stableAsset) !== null && _f !== void 0 ? _f : this._stableAsset,
25460
25974
  });
25461
25975
  }
25462
25976
  /**
@@ -26217,6 +26731,7 @@ class PortfolioFacade {
26217
26731
  */
26218
26732
  class Kasu {
26219
26733
  constructor(sdk, chainConfig, signerOrProvider, configOverrides) {
26734
+ var _a;
26220
26735
  this._sdk = sdk;
26221
26736
  this._chainConfig = chainConfig;
26222
26737
  this._signerOrProvider = signerOrProvider;
@@ -26228,7 +26743,7 @@ class Kasu {
26228
26743
  const isReadOnly = !ethers.Signer.isSigner(signerOrProvider);
26229
26744
  this.deposits = new DepositsFacade(sdk.UserLending, chainConfig.chainId.toString(), isReadOnly);
26230
26745
  this.portfolio = new PortfolioFacade(sdk.DataService, sdk.UserLending, sdk.Portfolio);
26231
- this.flows = new FlowsFacade(this.deposits, () => erc20Of(chainConfig, signerOrProvider), isReadOnly, chainConfig.contracts.LendingPoolManager);
26746
+ this.flows = new FlowsFacade(this.deposits, () => erc20Of(chainConfig, signerOrProvider), isReadOnly, chainConfig.contracts.LendingPoolManager, (_a = stableAssetOf(chainConfig)) === null || _a === void 0 ? void 0 : _a.address);
26232
26747
  }
26233
26748
  /**
26234
26749
  * Create a Kasu instance with built-in chain config.
@@ -26892,6 +27407,7 @@ function getTrancheDisplayName(trancheName, pool) {
26892
27407
  exports.APXIUM = APXIUM;
26893
27408
  exports.AU_ALPHA3 = AU_ALPHA3;
26894
27409
  exports.AU_MIN_CUMULATIVE_BY_STABLE = AU_MIN_CUMULATIVE_BY_STABLE;
27410
+ exports.BatchRevertedError = BatchRevertedError;
26895
27411
  exports.CHAIN_CONFIGS = CHAIN_CONFIGS;
26896
27412
  exports.CLEARING_WINDOW_SECONDS = CLEARING_WINDOW_SECONDS;
26897
27413
  exports.CONTRACT_TTL_MS = CONTRACT_TTL_MS;
@@ -26901,6 +27417,8 @@ exports.EPOCHS_IN_YEAR = EPOCHS_IN_YEAR;
26901
27417
  exports.Flow = Flow;
26902
27418
  exports.FlowStore = FlowStore;
26903
27419
  exports.FlowsFacade = FlowsFacade;
27420
+ exports.INVALID_ACCEPTANCE_SIGNATURE_MESSAGE = INVALID_ACCEPTANCE_SIGNATURE_MESSAGE;
27421
+ exports.INVALID_AUTH_SIGNATURE_MESSAGE = INVALID_AUTH_SIGNATURE_MESSAGE;
26904
27422
  exports.INVOICEMATE = INVOICEMATE;
26905
27423
  exports.Kasu = Kasu;
26906
27424
  exports.KasuSdk = KasuSdk;
@@ -26930,11 +27448,13 @@ exports.classifyWalletFailure = classifyWalletFailure;
26930
27448
  exports.compareTrancheSeniority = compareTrancheSeniority;
26931
27449
  exports.computeSettlementWindow = computeSettlementWindow;
26932
27450
  exports.countSubmissions = countSubmissions;
27451
+ exports.decodeRevert = decodeRevert;
26933
27452
  exports.deriveCycleDates = deriveCycleDates;
26934
27453
  exports.derivePoolStatus = derivePoolStatus;
26935
27454
  exports.deriveRequestState = deriveRequestState;
26936
27455
  exports.encodeDepositData = encodeDepositData;
26937
27456
  exports.epochRateToApy = epochRateToApy;
27457
+ exports.extractRevertData = extractRevertData;
26938
27458
  exports.fetchUnusedPoolIds = fetchUnusedPoolIds;
26939
27459
  exports.firstSubmissionTimestamp = firstSubmissionTimestamp;
26940
27460
  exports.floorToCents = floorToCents;