@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
@@ -1,8 +1,10 @@
1
1
  import { BigNumber } from 'ethers';
2
2
 
3
- import { classifyWalletFailure } from '../domain/wallet-errors';
3
+ import { SponsoredCall } from '../domain/sponsored-calls';
4
+ import { classifyWalletFailure, isUserRejected } from '../domain/wallet-errors';
4
5
  import { KycParams } from '../facade/types';
5
6
 
7
+ import { BatchRevertedError, BatchSubmissionError } from './batch-errors';
6
8
  import { Flow } from './flow';
7
9
  import { WaitableTransaction } from './observable';
8
10
 
@@ -29,6 +31,28 @@ import { WaitableTransaction } from './observable';
29
31
  * flow.subscribe(render);
30
32
  * await flow.start({ poolId, trancheId, amount: 'max', userAddress });
31
33
  * ```
34
+ *
35
+ * ## The sponsored path (2.10.0)
36
+ *
37
+ * Supply `submitCall` and the run never touches `withdraw` / `withdrawMax`:
38
+ * it encodes the ONE `requestWithdrawal` call itself (`encodeWithdrawal`,
39
+ * which `kasu.flows.withdraw()` defaults to `kasu.calls.requestWithdrawal`)
40
+ * and hands it to the port, whose `wait()` settles it. `state.sponsored` says
41
+ * which path this flow runs, from its first state.
42
+ *
43
+ * A submitted call's outcome is read with the batch path's taxonomy, and at
44
+ * its word: `BatchRevertedError` → `reverted`; `BatchSubmissionError` → its
45
+ * own outcome (`failed`, `pending`, `already-submitted`); a WALLET rejection
46
+ * (`isUserRejected`) thrown by `submitCall` itself → `cancelled`, exactly as a
47
+ * raw `depositBatch` throw is read — the lender declined to sign, nothing was
48
+ * posted; ANY other throw from `submitCall`, and every throw from `wait()`
49
+ * that is not one of the typed errors, → `pending`. The SDK cannot know
50
+ * whether an unrecognised error left before or after the call was accepted,
51
+ * and calling that `failed` invites a second withdrawal request.
52
+ * `submitSponsoredCall` is the port body that already speaks this; note that
53
+ * a rejection raised INSIDE its `post` is wrapped as `pending` (the 2.9.0
54
+ * rule for `post`), so sign in `submitCall` before calling it when the app
55
+ * wants `cancelled`.
32
56
  */
33
57
 
34
58
  // ---------------------------------------------------------------------------
@@ -52,7 +76,31 @@ export type WithdrawStep = 'kyc' | 'request';
52
76
 
53
77
  export type WithdrawFailure =
54
78
  | { step: WithdrawStep; reason: 'cancelled' }
55
- | { step: WithdrawStep; reason: 'failed'; error: unknown };
79
+ | { step: WithdrawStep; reason: 'failed'; error: unknown }
80
+ | {
81
+ /**
82
+ * Sponsored path only: the call is KNOWN to revert (or reverted) —
83
+ * nothing was requested. `revertError` is the contract's error name
84
+ * (`'CannotProcessRequestIfClearingIsPending'`,
85
+ * `'InsufficientSharesBalance'`, …) or `''`.
86
+ */
87
+ step: 'request';
88
+ reason: 'reverted';
89
+ revertError: string;
90
+ error: unknown;
91
+ }
92
+ | {
93
+ /**
94
+ * Sponsored path only — NOT failures. `pending`: the call was, or
95
+ * may have been, accepted and nothing proves whether it ran; never
96
+ * render it as a failure, which invites a second request.
97
+ * `already-submitted`: the port refused it because an earlier one
98
+ * was proven.
99
+ */
100
+ step: 'request';
101
+ reason: 'pending' | 'already-submitted';
102
+ error: unknown;
103
+ };
56
104
 
57
105
  // ---------------------------------------------------------------------------
58
106
  // Ports, input, state
@@ -63,9 +111,7 @@ export interface WithdrawPorts {
63
111
  * Build the Nexera KYC params for the pre-check. Defaults to
64
112
  * `kasu.deposits.buildKycParams`, exactly as the deposit flow's does.
65
113
  */
66
- buildKycParams?(
67
- userAddress: `0x${string}`,
68
- ): KycParams | Promise<KycParams>;
114
+ buildKycParams?(userAddress: `0x${string}`): KycParams | Promise<KycParams>;
69
115
  /**
70
116
  * Exchange those params for a signature at the consumer's own backend, and
71
117
  * so verify the lender's KYC BEFORE the wallet is opened — a lapsed one
@@ -90,6 +136,33 @@ export interface WithdrawPorts {
90
136
  trancheId: string,
91
137
  userAddress: string,
92
138
  ): Promise<WaitableTransaction>;
139
+ /**
140
+ * Submit ONE encoded call through a sponsored submitter (a gas-sponsoring
141
+ * relay) and return a handle whose `wait()` settles it. Supplying it is
142
+ * what turns the sponsored path on; it has no default, because how a call
143
+ * is relayed is the application's business. Post `call.kind` and
144
+ * `[call.call]` exactly as given — `submitSponsoredCall` does this, with
145
+ * the polling and the receipt proof.
146
+ *
147
+ * Throw `BatchRevertedError` for a known revert and
148
+ * `BatchSubmissionError(code, 'failed')` ONLY when nothing was sent. A
149
+ * wallet rejection thrown here (the lender declined to sign the call,
150
+ * `isUserRejected`) is `cancelled`; any other throw — here or from
151
+ * `wait()` — is read as `pending`.
152
+ */
153
+ submitCall?(call: SponsoredCall): Promise<WaitableTransaction>;
154
+ /**
155
+ * Encode the withdrawal for `submitCall`. Defaults (in
156
+ * `kasu.flows.withdraw()`) to `kasu.calls.requestWithdrawal`, which
157
+ * converts the amount to shares exactly as `withdraw` / `withdrawMax`
158
+ * do. Only read when `submitCall` is supplied.
159
+ */
160
+ encodeWithdrawal?(params: {
161
+ poolId: string;
162
+ trancheId: string;
163
+ amount: BigNumber | 'max';
164
+ userAddress: string;
165
+ }): Promise<SponsoredCall>;
93
166
  }
94
167
 
95
168
  export interface WithdrawFlowInput {
@@ -99,10 +172,11 @@ export interface WithdrawFlowInput {
99
172
  * The amount in BASE units, or the literal `'max'`.
100
173
  *
101
174
  * `'max'` is not "the balance as I last read it": it routes to the
102
- * all-shares contract call, which resolves the balance on chain at
103
- * execution. Deciding max-ness from a number the consumer read a moment ago
104
- * is how dust gets stranded in a tranche, so the decision is a code, made
105
- * by the consumer, and carried here intact.
175
+ * all-shares request, whose share count is the lender's tranche
176
+ * `balanceOf`, read by the SDK at submission rather than converted from
177
+ * an asset figure. Deciding max-ness from a number the consumer read a
178
+ * moment ago is how dust gets stranded in a tranche, so the decision is a
179
+ * code, made by the consumer, and carried here intact.
106
180
  */
107
181
  amount: BigNumber | 'max';
108
182
  userAddress: `0x${string}`;
@@ -113,6 +187,11 @@ export interface WithdrawState {
113
187
  step: WithdrawStep | null;
114
188
  /** True when this run routed through the all-shares call. */
115
189
  isMax: boolean;
190
+ /**
191
+ * True when this flow submits through `submitCall` — fixed when the flow
192
+ * is built, so it is right from the first state, `idle` included.
193
+ */
194
+ sponsored: boolean;
116
195
  failure: WithdrawFailure | null;
117
196
  }
118
197
 
@@ -120,6 +199,7 @@ const INITIAL: WithdrawState = {
120
199
  phase: 'idle',
121
200
  step: null,
122
201
  isMax: false,
202
+ sponsored: false,
123
203
  failure: null,
124
204
  };
125
205
 
@@ -127,13 +207,57 @@ const INITIAL: WithdrawState = {
127
207
  export const NO_KYC_PARAMS_MESSAGE =
128
208
  'WithdrawFlow: getKycSignature was supplied without buildKycParams; build the flow with kasu.flows.withdraw() or pass both';
129
209
 
210
+ /** What a sponsored run with no `encodeWithdrawal` to call fails with. */
211
+ export const NO_WITHDRAWAL_ENCODER_MESSAGE =
212
+ 'WithdrawFlow: submitCall was supplied without encodeWithdrawal; build the flow with kasu.flows.withdraw() or pass both';
213
+
214
+ /** What a sponsored run fails with when the encoder returned another kind of call. */
215
+ export const NOT_A_WITHDRAWAL_CALL_MESSAGE =
216
+ 'WithdrawFlow: encodeWithdrawal returned a call that is not a withdrawal';
217
+
218
+ /**
219
+ * A `submitCall` / `wait()` rejection, read with the batch path's taxonomy. A
220
+ * submission outcome is taken at its word FIRST — never through the wallet
221
+ * classifier, which would read a relay's "request rejected" as the lender's
222
+ * own doing. Then, from `submitCall` only, a wallet rejection is `cancelled`:
223
+ * the lender declined to sign and nothing was posted — the same reading a raw
224
+ * `depositBatch` throw gets. From `wait()` the call has already left, so a
225
+ * rejection-worded error there is as ambiguous as any other. Everything
226
+ * unrecognised is `pending`: it may have left.
227
+ */
228
+ function classifySponsored(
229
+ err: unknown,
230
+ from: 'submit' | 'wait',
231
+ ): WithdrawFailure {
232
+ if (err instanceof BatchSubmissionError) {
233
+ return err.outcome === 'failed'
234
+ ? { step: 'request', reason: 'failed', error: err }
235
+ : { step: 'request', reason: err.outcome, error: err };
236
+ }
237
+ if (err instanceof BatchRevertedError) {
238
+ return {
239
+ step: 'request',
240
+ reason: 'reverted',
241
+ revertError: err.revertError,
242
+ error: err,
243
+ };
244
+ }
245
+ if (from === 'submit' && isUserRejected(err)) {
246
+ return { step: 'request', reason: 'cancelled' };
247
+ }
248
+ return { step: 'request', reason: 'pending', error: err };
249
+ }
250
+
130
251
  // ---------------------------------------------------------------------------
131
252
  // The flow
132
253
  // ---------------------------------------------------------------------------
133
254
 
134
255
  export class WithdrawFlow extends Flow<WithdrawState, WithdrawFlowInput> {
135
256
  constructor(private readonly _ports: WithdrawPorts) {
136
- super(INITIAL);
257
+ super({
258
+ ...INITIAL,
259
+ sponsored: typeof _ports.submitCall === 'function',
260
+ });
137
261
  }
138
262
 
139
263
  protected async _run(
@@ -172,6 +296,11 @@ export class WithdrawFlow extends Flow<WithdrawState, WithdrawFlowInput> {
172
296
  if (!this._store.isCurrent(token)) return;
173
297
  }
174
298
 
299
+ if (ports.submitCall) {
300
+ await this._runSponsored(input, token, isMax);
301
+ return;
302
+ }
303
+
175
304
  // 2. The submission. This one IS a wallet call, so it keeps the
176
305
  // `cancelled` / `failed` split.
177
306
  this._store.patch(
@@ -207,4 +336,84 @@ export class WithdrawFlow extends Flow<WithdrawState, WithdrawFlowInput> {
207
336
 
208
337
  this._store.patch({ phase: 'success' }, token);
209
338
  }
339
+
340
+ /**
341
+ * 2'. The sponsored submission: encode, re-check the run, submit, settle.
342
+ *
343
+ * The encode reads the chain (the shares), so a run abandoned during it
344
+ * must stop BEFORE `submitCall` — nothing has been sent yet, and an
345
+ * abandoned run must never send. A throw while encoding is `failed`:
346
+ * nothing left. After `submitCall` is called, only the taxonomy decides.
347
+ */
348
+ private async _runSponsored(
349
+ input: WithdrawFlowInput,
350
+ token: number,
351
+ isMax: boolean,
352
+ ): Promise<void> {
353
+ const ports = this._ports;
354
+ // Only reached when `submitCall` is supplied; narrows it for below.
355
+ if (!ports.submitCall) return;
356
+ this._store.patch(
357
+ { phase: 'request-sign', step: 'request', isMax },
358
+ token,
359
+ );
360
+
361
+ let call: SponsoredCall;
362
+ try {
363
+ if (!ports.encodeWithdrawal) {
364
+ throw new Error(NO_WITHDRAWAL_ENCODER_MESSAGE);
365
+ }
366
+ call = await ports.encodeWithdrawal({
367
+ poolId: input.poolId,
368
+ trancheId: input.trancheId,
369
+ amount: input.amount,
370
+ userAddress: input.userAddress,
371
+ });
372
+ if (
373
+ (call as Partial<SponsoredCall> | null)?.kind !== 'withdrawal'
374
+ ) {
375
+ throw new Error(NOT_A_WITHDRAWAL_CALL_MESSAGE);
376
+ }
377
+ } catch (err) {
378
+ this._store.patch(
379
+ {
380
+ phase: 'error',
381
+ failure: { step: 'request', reason: 'failed', error: err },
382
+ },
383
+ token,
384
+ );
385
+ return;
386
+ }
387
+ if (!this._store.isCurrent(token)) return;
388
+
389
+ let tx: WaitableTransaction;
390
+ try {
391
+ tx = await ports.submitCall(call);
392
+ } catch (err) {
393
+ this._store.patch(
394
+ { phase: 'error', failure: classifySponsored(err, 'submit') },
395
+ token,
396
+ );
397
+ return;
398
+ }
399
+ // The call has left. A run abandoned meanwhile stops HERE, without
400
+ // `wait()`: nothing it could learn would be published, and a sponsored
401
+ // `wait()` is minutes of polling against the submitter, not a cheap
402
+ // receipt read. The submission itself cannot be taken back.
403
+ if (!this._store.isCurrent(token)) return;
404
+ this._store.patch({ phase: 'request-confirm' }, token);
405
+
406
+ try {
407
+ await tx.wait();
408
+ } catch (err) {
409
+ this._store.patch(
410
+ { phase: 'error', failure: classifySponsored(err, 'wait') },
411
+ token,
412
+ );
413
+ return;
414
+ }
415
+ if (!this._store.isCurrent(token)) return;
416
+
417
+ this._store.patch({ phase: 'success' }, token);
418
+ }
210
419
  }
@@ -53,6 +53,7 @@ export const getPoolOverviewQuery = (ids?: string[]): string => gql`
53
53
  fixedTermDepositStatus
54
54
  fixedTermDepositAllowlist{
55
55
  id
56
+ isAllowlisted
56
57
  user{
57
58
  id
58
59
  }
@@ -278,17 +278,50 @@ export class UserLending {
278
278
  );
279
279
  }
280
280
 
281
- async requestWithdrawalInAsset(
282
- lendingPool: string,
281
+ /**
282
+ * The tranche shares a withdrawal of `assetAmount` requests — the
283
+ * tranche's own `convertToShares`, read now. `requestWithdrawalInAsset`
284
+ * and the sponsored `kasu.calls.requestWithdrawal` both use this, so the
285
+ * two paths cannot convert differently.
286
+ */
287
+ async getWithdrawalSharesForAssets(
283
288
  tranche: string,
284
289
  assetAmount: BigNumberish,
285
- ): Promise<ContractTransaction> {
290
+ ): Promise<BigNumber> {
286
291
  const trancheContract: ILendingPoolTrancheAbi =
287
292
  ILendingPoolTrancheAbi__factory.connect(
288
293
  tranche,
289
294
  this._signerOrProvider,
290
295
  );
291
- const shareAmount = await trancheContract.convertToShares(assetAmount);
296
+ return await trancheContract.convertToShares(assetAmount);
297
+ }
298
+
299
+ /**
300
+ * Every tranche share `user` holds — what a max withdrawal requests
301
+ * (`balanceOf(user)` on the tranche, read now). Shared by
302
+ * `requestWithdrawalMax` and the sponsored `kasu.calls.requestWithdrawal`.
303
+ */
304
+ async getWithdrawalSharesMax(
305
+ tranche: string,
306
+ user: string,
307
+ ): Promise<BigNumber> {
308
+ const trancheContract: ILendingPoolTrancheAbi =
309
+ ILendingPoolTrancheAbi__factory.connect(
310
+ tranche,
311
+ this._signerOrProvider,
312
+ );
313
+ return await trancheContract['balanceOf(address)'](user);
314
+ }
315
+
316
+ async requestWithdrawalInAsset(
317
+ lendingPool: string,
318
+ tranche: string,
319
+ assetAmount: BigNumberish,
320
+ ): Promise<ContractTransaction> {
321
+ const shareAmount = await this.getWithdrawalSharesForAssets(
322
+ tranche,
323
+ assetAmount,
324
+ );
292
325
 
293
326
  return await this._lendingPoolManagerAbi.requestWithdrawal(
294
327
  lendingPool,
@@ -311,12 +344,7 @@ export class UserLending {
311
344
  tranche: string,
312
345
  user: string,
313
346
  ): Promise<ContractTransaction> {
314
- const trancheContract: ILendingPoolTrancheAbi =
315
- ILendingPoolTrancheAbi__factory.connect(
316
- tranche,
317
- this._signerOrProvider,
318
- );
319
- const userShares = await trancheContract['balanceOf(address)'](user);
347
+ const userShares = await this.getWithdrawalSharesMax(tranche, user);
320
348
 
321
349
  return await this._lendingPoolManagerAbi.requestWithdrawal(
322
350
  lendingPool,