@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.
@@ -45,16 +45,41 @@ import { WaitableTransaction } from './observable';
45
45
  * 8. **`request-sign` / `request-confirm`** — KYC signature, then the deposit,
46
46
  * then the receipt.
47
47
  *
48
+ * ## The batch path
49
+ *
50
+ * A consumer that can submit several calls as ONE atomic transaction supplies
51
+ * the optional `depositBatch` port. When the allowance is short, that port is
52
+ * present and the stable token is known, steps 7 and 8 become one: the SDK
53
+ * encodes `approve(spender, amount)` and `requestDepositWithKyc(...)` itself
54
+ * and hands both to the port. Because a batch has no wallet prompt per call,
55
+ * the run first PARKS on `awaiting-authorise` until the consumer calls
56
+ * `authoriseTransfer()` (or `declineTransfer()`) — nothing moves money
57
+ * without an explicit authorisation. The TTL is checked again after it, and
58
+ * the KYC signature is fetched only then, because it carries a block
59
+ * expiration of its own. Without the port, or with the allowance already
60
+ * sufficient, the run is exactly the two-transaction pipeline above.
61
+ *
48
62
  * ## Failure codes
49
63
  *
50
64
  * A wallet rejection (`isUserRejected`) is `'cancelled'` — the lender changed
51
65
  * their mind, and telling them something broke would be a lie. Only a WALLET
52
66
  * call is ever classified that way: the HTTP ports are always `'failed'`,
53
67
  * because a backend that words a refusal "declined" did not involve the
54
- * lender's wallet. A reverted gas estimate on the request step
55
- * (`isUnpredictableGas`) is `'insufficient-balance'` — nothing was refused, the
56
- * transaction simply cannot succeed as composed. Everything else is `'failed'`
57
- * and carries the original error for the consumer's crash reporter.
68
+ * lender's wallet. A revert on the request step is read for WHAT reverted: a
69
+ * protocol error the ABI declares is `'reverted'` with the name, and only a
70
+ * token balance or allowance failure — or a revert nothing can decode — stays
71
+ * `'insufficient-balance'`. Everything else is `'failed'` and carries the
72
+ * original error for the consumer's crash reporter.
73
+ *
74
+ * ## Both signatures are checked before they are used
75
+ *
76
+ * A wallet is not obliged to hand back 65 bytes, and `encodeDepositData` will
77
+ * not catch one that does not: `defaultAbiCoder` encodes any even-length hex
78
+ * as `bytes`, `0x` included, so a malformed acceptance signature becomes a
79
+ * `depositData` blob that is broadcast, mined, and then can never be verified
80
+ * by the agreements service. Both personal signs are therefore length-checked
81
+ * where they are taken, and a bad one fails the step it was taken on rather
82
+ * than the one it would eventually have broken.
58
83
  *
59
84
  * ```ts
60
85
  * const flow = new DepositFlow(ports);
@@ -68,7 +93,12 @@ import { WaitableTransaction } from './observable';
68
93
  * Where the run is. `success`, `declined` and `error` are terminal; everything
69
94
  * else is in flight.
70
95
  */
71
- export type DepositPhase = 'idle' | 'generating-sign' | 'generating-fetch' | 'awaiting-accept' | 'accepting-sign' | 'approve' | 'request-sign' | 'request-confirm' | 'success' | 'declined' | 'error';
96
+ export type DepositPhase = 'idle' | 'generating-sign' | 'generating-fetch' | 'awaiting-accept' | 'accepting-sign' | 'approve'
97
+ /**
98
+ * Batch path only: the run is PARKED until `authoriseTransfer()` or
99
+ * `declineTransfer()`. Step `request`.
100
+ */
101
+ | 'awaiting-authorise' | 'request-sign' | 'request-confirm' | 'success' | 'declined' | 'error';
72
102
  /**
73
103
  * The four steps a lender sees as a badge. `approve` drops out of the sequence
74
104
  * when the allowance already covers the deposit, which is why `stepIndex` and
@@ -93,11 +123,55 @@ export type DepositFailure = {
93
123
  step: 'request';
94
124
  reason: 'insufficient-balance';
95
125
  error: unknown;
126
+ } | {
127
+ step: 'request';
128
+ reason: 'reverted';
129
+ /**
130
+ * The custom error the contract reverted with, exactly as the ABI
131
+ * declares it — `'ClearingIsPending'`, `'LendingPoolIsStopped'`,
132
+ * `'UserNotKycd'`, and the rest of
133
+ * `ILendingPoolManagerAbi` / `IKasuAllowListAbi`.
134
+ *
135
+ * Render `reverted` with generic copy and special-case only the
136
+ * names you have words for: a contract upgrade can add an error, and
137
+ * a consumer that assumed the set was closed would have nothing to
138
+ * show for the new one.
139
+ */
140
+ revertError: string;
141
+ error: unknown;
96
142
  } | {
97
143
  step: 'request';
98
144
  reason: 'contract-expired';
99
145
  };
100
146
  export type { WaitableTransaction };
147
+ /**
148
+ * One call of an atomic batch: a target and its ABI-encoded calldata. No
149
+ * value — neither call the deposit batch makes sends the native token.
150
+ */
151
+ export interface BatchCall {
152
+ to: string;
153
+ data: string;
154
+ }
155
+ /**
156
+ * What a `depositBatch` port throws when it KNOWS the batch reverted — from a
157
+ * simulation before sending, or from the submitter refusing to broadcast a
158
+ * batch that would revert. The run ends `{ step: 'request', reason:
159
+ * 'reverted', revertError }`.
160
+ *
161
+ * `revertError` is the custom error the contract named (`'ClearingIsPending'`,
162
+ * `'UserNotKycd'`, …), exactly as the ABI declares it. Pass `''` when the
163
+ * batch is known to revert but not why: the consumer then renders its generic
164
+ * `reverted` copy, which is what an unknown name gets anyway.
165
+ *
166
+ * Any OTHER throw from the port is read the way a direct deposit's is — a
167
+ * wallet rejection is `cancelled`, a decodable revert is `reverted`, and
168
+ * everything else is `failed` with the error kept. A port that cannot tell is
169
+ * better off throwing what it has than guessing a name.
170
+ */
171
+ export declare class BatchRevertedError extends Error {
172
+ readonly revertError: string;
173
+ constructor(revertError: string, message?: string);
174
+ }
101
175
  /** What the KYC signing service hands back. */
102
176
  export interface KycSignature {
103
177
  signature: string;
@@ -190,10 +264,34 @@ export interface DepositPorts {
190
264
  approve(spender: string, amount: BigNumber): Promise<WaitableTransaction>;
191
265
  /** `requestDepositWithKyc`. Defaults to `kasu.deposits.deposit`. */
192
266
  deposit(params: DepositParams): Promise<WaitableTransaction>;
267
+ /**
268
+ * OPTIONAL, and with no default: submit several calls as ONE atomic
269
+ * transaction — all of them apply, or none does. How is the consumer's
270
+ * business; the SDK owns what goes in and in which order.
271
+ *
272
+ * When supplied, the allowance is short and the flow knows the stable
273
+ * token (`DepositFlowOptions.stableAsset`), the run takes the batch path:
274
+ * it parks on `awaiting-authorise`, then hands this port exactly two calls
275
+ * —
276
+ *
277
+ * 0. `stableAsset.approve(spender, amount)` — the EXACT deposit amount,
278
+ * never `MaxUint256`;
279
+ * 1. `spender.requestDepositWithKyc(pool, tranche, amount, '0x',
280
+ * fixedTermConfigId, depositData, { blockExpiration, signature })` —
281
+ * the call the default `deposit` port makes.
282
+ *
283
+ * and neither `approve` nor `deposit` is called. Resolve with something
284
+ * whose `wait()` settles when the batch is mined. Throw
285
+ * `BatchRevertedError` when the batch is known to revert.
286
+ */
287
+ depositBatch?(calls: BatchCall[]): Promise<WaitableTransaction>;
193
288
  /** ms-epoch clock. Defaults to `Date.now`; injected so the TTL is testable. */
194
289
  now?(): number;
195
290
  }
196
- /** Construction options. `kasu.flows.deposit()` fills `spender` in. */
291
+ /**
292
+ * Construction options. `kasu.flows.deposit()` fills `spender` and
293
+ * `stableAsset` in.
294
+ */
197
295
  export interface DepositFlowOptions {
198
296
  /** Agreement validity window; defaults to `CONTRACT_TTL_MS`. */
199
297
  contractTtlMs?: number;
@@ -204,6 +302,17 @@ export interface DepositFlowOptions {
204
302
  * deposit port calls.
205
303
  */
206
304
  spender?: string;
305
+ /**
306
+ * The ERC-20 the deposit is paid in — the target of the batch path's
307
+ * approve call. `kasu.flows.deposit()` passes this chain's
308
+ * `stableAsset.address`.
309
+ *
310
+ * Without it the batch path is never taken, even when `depositBatch` is
311
+ * supplied: an approve encoded against a guessed token is an approval
312
+ * granted on the wrong contract inside a transaction that then reverts.
313
+ * The run falls back to the `approve` and `deposit` ports instead.
314
+ */
315
+ stableAsset?: string;
207
316
  }
208
317
  export interface DepositFlowInput {
209
318
  poolId: string;
@@ -241,10 +350,24 @@ export interface DepositState {
241
350
  step: DepositStep | null;
242
351
  /** 1-based badge position of `step`; `0` while idle. */
243
352
  stepIndex: number;
244
- /** `4`, or `3` when the allowance already covers the deposit. */
353
+ /**
354
+ * `4`, or `3` when the allowance already covers the deposit or the run
355
+ * is batched.
356
+ */
245
357
  stepTotal: number;
246
- /** Whether the approve step is in scope for this run. */
358
+ /**
359
+ * Whether the separate approve STEP is in scope for this run. `false`
360
+ * on a batched run too: its approval rides inside the request step's
361
+ * single transaction, so it never enters `approve` and counts as a
362
+ * three-step run on every badge.
363
+ */
247
364
  approvalRequired: boolean;
365
+ /**
366
+ * Whether this run approves and deposits as ONE atomic batch through the
367
+ * `depositBatch` port. Decided by the allowance pre-check, before the
368
+ * first signature, and `false` on every two-transaction run.
369
+ */
370
+ batched: boolean;
248
371
  /** The generated agreement, from `generating-fetch` onwards. */
249
372
  contract: GenerateContractResponse | null;
250
373
  /** Set with `phase: 'error'`, cleared by `reset()`. */
@@ -254,10 +377,15 @@ export interface DepositState {
254
377
  export declare const CONTRACT_TTL_MS: number;
255
378
  /** What a run without a spender, from either source, fails with. */
256
379
  export declare const NO_SPENDER_MESSAGE = "DepositFlow: no ERC-20 spender; build the flow with kasu.flows.deposit() or pass `spender` on the input";
380
+ /** What a malformed auth signature fails the generate step with. */
381
+ export declare const INVALID_AUTH_SIGNATURE_MESSAGE = "DepositFlow: the wallet returned a malformed authentication signature; expected 65 bytes of 0x-prefixed hex";
382
+ /** What a malformed acceptance signature fails the confirm step with. */
383
+ export declare const INVALID_ACCEPTANCE_SIGNATURE_MESSAGE = "DepositFlow: the wallet returned a malformed acceptance signature; expected 65 bytes of 0x-prefixed hex";
257
384
  export declare class DepositFlow extends Flow<DepositState, DepositFlowInput> {
258
385
  private readonly _ports;
259
386
  private readonly _ttlMs;
260
387
  private readonly _defaultSpender;
388
+ private readonly _stableAsset;
261
389
  private readonly _now;
262
390
  /**
263
391
  * The accept handshake. The run parks on this promise; `acceptContract`,
@@ -277,6 +405,12 @@ export declare class DepositFlow extends Flow<DepositState, DepositFlowInput> {
277
405
  * A stale token simply is not the current generation.
278
406
  */
279
407
  private _acceptingFor;
408
+ /**
409
+ * The batch path's authorisation handshake. Same shape and lifetime as
410
+ * `_accept`: the run parks on it, `authoriseTransfer`, `declineTransfer`
411
+ * and `reset` settle it, and it is cleared the moment it settles.
412
+ */
413
+ private _authorise;
280
414
  constructor(_ports: DepositPorts, opts?: DepositFlowOptions);
281
415
  /**
282
416
  * Sign the agreement and resume the parked run. A no-op when nothing is
@@ -292,10 +426,38 @@ export declare class DepositFlow extends Flow<DepositState, DepositFlowInput> {
292
426
  * out of a prompt that never answers.
293
427
  */
294
428
  declineContract(): void;
429
+ /**
430
+ * Batch path: authorise the single transaction that approves and deposits,
431
+ * and resume the parked run. A no-op when nothing is parked on
432
+ * `awaiting-authorise`, so a double tap cannot submit twice.
433
+ *
434
+ * The batch has no wallet prompt per call, so this is the lender's
435
+ * explicit consent to the transfer — call it only from their action.
436
+ */
437
+ authoriseTransfer(): void;
438
+ /**
439
+ * Batch path: refuse the transfer. The run ends on `error` with `{ step:
440
+ * 'request', reason: 'cancelled' }` and nothing is submitted. A no-op when
441
+ * nothing is parked on `awaiting-authorise`.
442
+ */
443
+ declineTransfer(): void;
295
444
  /** True only while THIS generation is waiting on the acceptance signature. */
296
445
  private _isAccepting;
297
446
  /** `reset()`: unpark the abandoned run and drop its handshake. */
298
447
  protected _onAbandon(): void;
299
448
  protected _run(input: DepositFlowInput, token: number): Promise<void>;
449
+ /**
450
+ * The batch path, from the accepted agreement to the receipt.
451
+ *
452
+ * 1. PARK on `awaiting-authorise`. A batch puts no wallet prompt in front
453
+ * of each call, so the flow waits for an explicit authorisation before
454
+ * any money moves. `declineTransfer()` ends it as `cancelled`.
455
+ * 2. Re-check the TTL: the lender may have idled on the authorisation as
456
+ * long as they did on the agreement.
457
+ * 3. Only now fetch the KYC signature — it has a block expiration of its
458
+ * own, and an authorisation left open must not spend it.
459
+ * 4. Encode both calls and hand them to `depositBatch`; then the receipt.
460
+ */
461
+ private _runBatch;
300
462
  private _fail;
301
463
  }