@kasufinance/kasu-sdk 2.7.1 → 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.
package/README.md CHANGED
@@ -225,8 +225,52 @@ await flow.acceptContract(); // signs it and carries on
225
225
  // or: flow.declineContract(); → phase 'declined', nothing submitted
226
226
  ```
227
227
 
228
+ ### One atomic transaction: the `depositBatch` port
229
+
230
+ If your application can submit several calls as ONE atomic transaction (all
231
+ apply or none does), supply the optional `depositBatch` port. When the
232
+ allowance is short, the run then approves and deposits in a single batch
233
+ instead of two transactions. You decide how the batch is sent; the SDK decides
234
+ what is in it:
235
+
236
+ ```ts
237
+ import { BatchRevertedError, type BatchCall } from '@kasufinance/kasu-sdk';
238
+
239
+ const flow = kasu.flows.deposit({
240
+ signMessage,
241
+ generateContract,
242
+ getKycSignature,
243
+ depositBatch: async (calls: BatchCall[]) => {
244
+ // calls[0]: stableAsset.approve(LendingPoolManager, amount) — EXACT amount
245
+ // calls[1]: LendingPoolManager.requestDepositWithKyc(…) — value 0
246
+ const tx = await submitAtomically(calls); // your mechanism
247
+ return tx; // anything with wait()
248
+ // Known to revert? throw new BatchRevertedError('ClearingIsPending')
249
+ },
250
+ });
251
+
252
+ // …after acceptContract(), a batched run parks on `awaiting-authorise`:
253
+ flow.authoriseTransfer(); // the lender's explicit go-ahead
254
+ // or: flow.declineTransfer(); → error { step: 'request', reason: 'cancelled' }
255
+ ```
256
+
257
+ - The batch path is taken only when the allowance is short, the port is
258
+ supplied and the chain's stable token is known (`kasu.flows.deposit()` fills
259
+ it from the chain config). Otherwise the run is exactly the two-transaction
260
+ pipeline.
261
+ - A batch has no wallet prompt per call, so the run **parks for an explicit
262
+ authorisation** before any money moves. The agreement's TTL is checked again
263
+ after it, and the KYC signature is fetched only then.
264
+ - `state.batched` is `true` from the first state of a batched run. It never
265
+ enters `approve`, so it counts as a three-step run (`stepTotal` 3).
266
+ - Throw `BatchRevertedError(revertError)` when the batch is known to revert →
267
+ `{ step: 'request', reason: 'reverted', revertError }`. Any other throw is
268
+ read like a direct deposit's: a wallet rejection is `cancelled`, everything
269
+ else `failed`.
270
+
228
271
  **Phases** — `idle`, `generating-sign`, `generating-fetch`, `awaiting-accept`,
229
- `accepting-sign`, `approve`, `request-sign`, `request-confirm`, and the
272
+ `accepting-sign`, `approve`, `awaiting-authorise` (batch path only),
273
+ `request-sign`, `request-confirm`, and the
230
274
  terminal `success` / `declined` / `error`. Each maps to one of four steps
231
275
  (`generate`, `confirm`, `approve`, `request`); `stepIndex` and `stepTotal` are
232
276
  published because the approve step drops out of the sequence when the allowance
@@ -25148,6 +25148,31 @@ function classifyWalletFailure(step, err) {
25148
25148
  : { step, reason: 'failed', error: err };
25149
25149
  }
25150
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
+ }
25151
25176
  /** Generated agreements are valid for five minutes upstream. */
25152
25177
  const CONTRACT_TTL_MS = 5 * 60 * 1000;
25153
25178
  /** What a run without a spender, from either source, fails with. */
@@ -25174,9 +25199,43 @@ const INITIAL$1 = {
25174
25199
  stepIndex: 0,
25175
25200
  stepTotal: 4,
25176
25201
  approvalRequired: true,
25202
+ batched: false,
25177
25203
  contract: null,
25178
25204
  failure: null,
25179
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
+ }
25180
25239
  /** 1-based badge position, with `approve` dropped when it is out of scope. */
25181
25240
  function stepIndexOf(step, approvalRequired) {
25182
25241
  const order = approvalRequired
@@ -25222,6 +25281,22 @@ function classifyRequest(err) {
25222
25281
  }
25223
25282
  return classifyWalletFailure('request', err);
25224
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
+ }
25225
25300
  // ---------------------------------------------------------------------------
25226
25301
  // The flow
25227
25302
  // ---------------------------------------------------------------------------
@@ -25248,8 +25323,15 @@ class DepositFlow extends Flow {
25248
25323
  * A stale token simply is not the current generation.
25249
25324
  */
25250
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;
25251
25332
  this._ttlMs = (_a = opts === null || opts === void 0 ? void 0 : opts.contractTtlMs) !== null && _a !== void 0 ? _a : CONTRACT_TTL_MS;
25252
25333
  this._defaultSpender = opts === null || opts === void 0 ? void 0 : opts.spender;
25334
+ this._stableAsset = opts === null || opts === void 0 ? void 0 : opts.stableAsset;
25253
25335
  // Called through the ports object, never captured off it: a consumer
25254
25336
  // whose clock is a method on its own object keeps its `this`.
25255
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(); };
@@ -25311,6 +25393,33 @@ class DepositFlow extends Flow {
25311
25393
  this._accept = null;
25312
25394
  bridge.settle({ kind: 'declined' });
25313
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
+ }
25314
25423
  /** True only while THIS generation is waiting on the acceptance signature. */
25315
25424
  _isAccepting() {
25316
25425
  return (this._acceptingFor !== null &&
@@ -25322,6 +25431,9 @@ class DepositFlow extends Flow {
25322
25431
  this._accept = null;
25323
25432
  this._acceptingFor = null;
25324
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' });
25325
25437
  }
25326
25438
  // -----------------------------------------------------------------------
25327
25439
  _run(input, token) {
@@ -25329,6 +25441,7 @@ class DepositFlow extends Flow {
25329
25441
  var _a;
25330
25442
  this._accept = null;
25331
25443
  this._acceptingFor = null;
25444
+ this._authorise = null;
25332
25445
  const ports = this._ports;
25333
25446
  const owner = input.userAddress.toLowerCase();
25334
25447
  // 0. The spender, from the input or from the chain config the facade
@@ -25349,21 +25462,32 @@ class DepositFlow extends Flow {
25349
25462
  // live, never cached: an exact-amount approval is fully consumed by
25350
25463
  // the deposit it paid for, so a stale allowance is exactly the value
25351
25464
  // that would wrongly skip the approve and revert the deposit.
25352
- let approvalRequired = true;
25465
+ let allowanceShort = true;
25353
25466
  try {
25354
25467
  const allowance = yield ports.readAllowance(owner, spender);
25355
- approvalRequired = allowance.lt(input.amount);
25468
+ allowanceShort = allowance.lt(input.amount);
25356
25469
  }
25357
25470
  catch (_b) {
25358
25471
  // A read failure is not a reason to skip an approval. Assume one is
25359
25472
  // needed: the cost is a redundant approve, the alternative is a
25360
25473
  // reverted deposit.
25361
- approvalRequired = true;
25474
+ allowanceShort = true;
25362
25475
  }
25363
25476
  if (!this._store.isCurrent(token))
25364
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;
25365
25488
  // 2. Generate — sign the auth message.
25366
- 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);
25367
25491
  const timestamp = this._now();
25368
25492
  const signedMessage = buildAuthMessage(input, owner, timestamp);
25369
25493
  let signature;
@@ -25465,6 +25589,18 @@ class DepositFlow extends Flow {
25465
25589
  });
25466
25590
  return;
25467
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
+ }
25468
25604
  // 6. Approve — the EXACT amount, never `MaxUint256`. House rule: an
25469
25605
  // unlimited allowance outlives the deposit it was granted for, and a
25470
25606
  // later exploit of the spender would drain a wallet that has long
@@ -25528,6 +25664,108 @@ class DepositFlow extends Flow {
25528
25664
  this._store.patch(Object.assign({ phase: 'success' }, badgeFor('request', approvalRequired)), token);
25529
25665
  });
25530
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
+ }
25531
25769
  _fail(token, approvalRequired, failure) {
25532
25770
  this._store.patch(Object.assign({ phase: 'error', failure }, badgeFor(failure.step, approvalRequired)), token);
25533
25771
  }
@@ -25680,11 +25918,18 @@ class FlowsFacade {
25680
25918
  * an address whose only wrong value grants an approval to the wrong
25681
25919
  * contract.
25682
25920
  */
25683
- _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) {
25684
25928
  this._deposits = _deposits;
25685
25929
  this._erc20 = _erc20;
25686
25930
  this._isReadOnly = _isReadOnly;
25687
25931
  this._spender = _spender;
25932
+ this._stableAsset = _stableAsset;
25688
25933
  }
25689
25934
  /**
25690
25935
  * A deposit pipeline. `readAllowance`, `approve`, `deposit` and
@@ -25697,9 +25942,13 @@ class FlowsFacade {
25697
25942
  * delete the default it was meant to keep, and
25698
25943
  * `approve: sponsoredGas ? sponsoredApprove : undefined` is exactly how a
25699
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.
25700
25949
  */
25701
25950
  deposit(ports, opts) {
25702
- var _a, _b, _c, _d, _e;
25951
+ var _a, _b, _c, _d, _e, _f;
25703
25952
  return new DepositFlow({
25704
25953
  signMessage: ports.signMessage,
25705
25954
  generateContract: ports.generateContract,
@@ -25714,12 +25963,14 @@ class FlowsFacade {
25714
25963
  return yield this._erc20().approve(spender, amount);
25715
25964
  })),
25716
25965
  deposit: (_d = ports.deposit) !== null && _d !== void 0 ? _d : ((params) => this._deposits.deposit(params)),
25966
+ depositBatch: ports.depositBatch,
25717
25967
  now: ports.now,
25718
25968
  }, {
25719
25969
  contractTtlMs: opts === null || opts === void 0 ? void 0 : opts.contractTtlMs,
25720
25970
  // Per key here too, for the same reason the ports are: an
25721
25971
  // explicit `spender: undefined` must not delete the default.
25722
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,
25723
25974
  });
25724
25975
  }
25725
25976
  /**
@@ -26480,6 +26731,7 @@ class PortfolioFacade {
26480
26731
  */
26481
26732
  class Kasu {
26482
26733
  constructor(sdk, chainConfig, signerOrProvider, configOverrides) {
26734
+ var _a;
26483
26735
  this._sdk = sdk;
26484
26736
  this._chainConfig = chainConfig;
26485
26737
  this._signerOrProvider = signerOrProvider;
@@ -26491,7 +26743,7 @@ class Kasu {
26491
26743
  const isReadOnly = !ethers.Signer.isSigner(signerOrProvider);
26492
26744
  this.deposits = new DepositsFacade(sdk.UserLending, chainConfig.chainId.toString(), isReadOnly);
26493
26745
  this.portfolio = new PortfolioFacade(sdk.DataService, sdk.UserLending, sdk.Portfolio);
26494
- 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);
26495
26747
  }
26496
26748
  /**
26497
26749
  * Create a Kasu instance with built-in chain config.
@@ -27155,6 +27407,7 @@ function getTrancheDisplayName(trancheName, pool) {
27155
27407
  exports.APXIUM = APXIUM;
27156
27408
  exports.AU_ALPHA3 = AU_ALPHA3;
27157
27409
  exports.AU_MIN_CUMULATIVE_BY_STABLE = AU_MIN_CUMULATIVE_BY_STABLE;
27410
+ exports.BatchRevertedError = BatchRevertedError;
27158
27411
  exports.CHAIN_CONFIGS = CHAIN_CONFIGS;
27159
27412
  exports.CLEARING_WINDOW_SECONDS = CLEARING_WINDOW_SECONDS;
27160
27413
  exports.CONTRACT_TTL_MS = CONTRACT_TTL_MS;