@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.
@@ -75,6 +75,12 @@ export class FlowsFacade {
75
75
  * contract.
76
76
  */
77
77
  private readonly _spender: string,
78
+ /**
79
+ * This chain's stable token address — the target of the batch path's
80
+ * approve. `undefined` for a hand-built config without `stableAsset`,
81
+ * in which case a run never batches: it does not guess a token.
82
+ */
83
+ private readonly _stableAsset?: string,
78
84
  ) {}
79
85
 
80
86
  /**
@@ -88,6 +94,10 @@ export class FlowsFacade {
88
94
  * delete the default it was meant to keep, and
89
95
  * `approve: sponsoredGas ? sponsoredApprove : undefined` is exactly how a
90
96
  * consumer writes a conditional override.
97
+ *
98
+ * `depositBatch` has NO default: how several calls become one atomic
99
+ * transaction is the application's business. Supplying it lets a run
100
+ * whose allowance is short approve and deposit in one batch.
91
101
  */
92
102
  deposit(
93
103
  ports: DepositFlowPortOverrides,
@@ -122,6 +132,7 @@ export class FlowsFacade {
122
132
  ports.deposit ??
123
133
  ((params): Promise<WaitableTransaction> =>
124
134
  this._deposits.deposit(params)),
135
+ depositBatch: ports.depositBatch,
125
136
  now: ports.now,
126
137
  },
127
138
  {
@@ -129,6 +140,7 @@ export class FlowsFacade {
129
140
  // Per key here too, for the same reason the ports are: an
130
141
  // explicit `spender: undefined` must not delete the default.
131
142
  spender: opts?.spender ?? this._spender,
143
+ stableAsset: opts?.stableAsset ?? this._stableAsset,
132
144
  },
133
145
  );
134
146
  }
@@ -98,6 +98,7 @@ export class Kasu {
98
98
  () => erc20Of(chainConfig, signerOrProvider),
99
99
  isReadOnly,
100
100
  chainConfig.contracts.LendingPoolManager,
101
+ stableAssetOf(chainConfig)?.address,
101
102
  );
102
103
  }
103
104
 
@@ -1,5 +1,7 @@
1
1
  import { BigNumber, utils } from 'ethers';
2
2
 
3
+ import { IERC20MetadataAbi__factory } from '../contracts/factories/IERC20MetadataAbi__factory';
4
+ import { ILendingPoolManagerAbi__factory } from '../contracts/factories/ILendingPoolManagerAbi__factory';
3
5
  import {
4
6
  asContractType,
5
7
  buildLegacyContractRequestMessage,
@@ -59,6 +61,20 @@ import { WaitableTransaction } from './observable';
59
61
  * 8. **`request-sign` / `request-confirm`** — KYC signature, then the deposit,
60
62
  * then the receipt.
61
63
  *
64
+ * ## The batch path
65
+ *
66
+ * A consumer that can submit several calls as ONE atomic transaction supplies
67
+ * the optional `depositBatch` port. When the allowance is short, that port is
68
+ * present and the stable token is known, steps 7 and 8 become one: the SDK
69
+ * encodes `approve(spender, amount)` and `requestDepositWithKyc(...)` itself
70
+ * and hands both to the port. Because a batch has no wallet prompt per call,
71
+ * the run first PARKS on `awaiting-authorise` until the consumer calls
72
+ * `authoriseTransfer()` (or `declineTransfer()`) — nothing moves money
73
+ * without an explicit authorisation. The TTL is checked again after it, and
74
+ * the KYC signature is fetched only then, because it carries a block
75
+ * expiration of its own. Without the port, or with the allowance already
76
+ * sufficient, the run is exactly the two-transaction pipeline above.
77
+ *
62
78
  * ## Failure codes
63
79
  *
64
80
  * A wallet rejection (`isUserRejected`) is `'cancelled'` — the lender changed
@@ -105,6 +121,11 @@ export type DepositPhase =
105
121
  | 'awaiting-accept'
106
122
  | 'accepting-sign'
107
123
  | 'approve'
124
+ /**
125
+ * Batch path only: the run is PARKED until `authoriseTransfer()` or
126
+ * `declineTransfer()`. Step `request`.
127
+ */
128
+ | 'awaiting-authorise'
108
129
  | 'request-sign'
109
130
  | 'request-confirm'
110
131
  | 'success'
@@ -154,6 +175,48 @@ export type DepositFailure =
154
175
 
155
176
  export type { WaitableTransaction };
156
177
 
178
+ /**
179
+ * One call of an atomic batch: a target and its ABI-encoded calldata. No
180
+ * value — neither call the deposit batch makes sends the native token.
181
+ */
182
+ export interface BatchCall {
183
+ to: string;
184
+ data: string;
185
+ }
186
+
187
+ /**
188
+ * What a `depositBatch` port throws when it KNOWS the batch reverted — from a
189
+ * simulation before sending, or from the submitter refusing to broadcast a
190
+ * batch that would revert. The run ends `{ step: 'request', reason:
191
+ * 'reverted', revertError }`.
192
+ *
193
+ * `revertError` is the custom error the contract named (`'ClearingIsPending'`,
194
+ * `'UserNotKycd'`, …), exactly as the ABI declares it. Pass `''` when the
195
+ * batch is known to revert but not why: the consumer then renders its generic
196
+ * `reverted` copy, which is what an unknown name gets anyway.
197
+ *
198
+ * Any OTHER throw from the port is read the way a direct deposit's is — a
199
+ * wallet rejection is `cancelled`, a decodable revert is `reverted`, and
200
+ * everything else is `failed` with the error kept. A port that cannot tell is
201
+ * better off throwing what it has than guessing a name.
202
+ */
203
+ export class BatchRevertedError extends Error {
204
+ readonly revertError: string;
205
+
206
+ constructor(revertError: string, message?: string) {
207
+ super(
208
+ message ??
209
+ `DepositFlow: the batch reverted${
210
+ revertError ? ` with ${revertError}` : ''
211
+ }`,
212
+ );
213
+ this.name = 'BatchRevertedError';
214
+ this.revertError = revertError;
215
+ // Keeps `instanceof` honest under a down-levelled build.
216
+ Object.setPrototypeOf(this, BatchRevertedError.prototype);
217
+ }
218
+ }
219
+
157
220
  /** What the KYC signing service hands back. */
158
221
  export interface KycSignature {
159
222
  signature: string;
@@ -257,11 +320,35 @@ export interface DepositPorts {
257
320
  approve(spender: string, amount: BigNumber): Promise<WaitableTransaction>;
258
321
  /** `requestDepositWithKyc`. Defaults to `kasu.deposits.deposit`. */
259
322
  deposit(params: DepositParams): Promise<WaitableTransaction>;
323
+ /**
324
+ * OPTIONAL, and with no default: submit several calls as ONE atomic
325
+ * transaction — all of them apply, or none does. How is the consumer's
326
+ * business; the SDK owns what goes in and in which order.
327
+ *
328
+ * When supplied, the allowance is short and the flow knows the stable
329
+ * token (`DepositFlowOptions.stableAsset`), the run takes the batch path:
330
+ * it parks on `awaiting-authorise`, then hands this port exactly two calls
331
+ * —
332
+ *
333
+ * 0. `stableAsset.approve(spender, amount)` — the EXACT deposit amount,
334
+ * never `MaxUint256`;
335
+ * 1. `spender.requestDepositWithKyc(pool, tranche, amount, '0x',
336
+ * fixedTermConfigId, depositData, { blockExpiration, signature })` —
337
+ * the call the default `deposit` port makes.
338
+ *
339
+ * and neither `approve` nor `deposit` is called. Resolve with something
340
+ * whose `wait()` settles when the batch is mined. Throw
341
+ * `BatchRevertedError` when the batch is known to revert.
342
+ */
343
+ depositBatch?(calls: BatchCall[]): Promise<WaitableTransaction>;
260
344
  /** ms-epoch clock. Defaults to `Date.now`; injected so the TTL is testable. */
261
345
  now?(): number;
262
346
  }
263
347
 
264
- /** Construction options. `kasu.flows.deposit()` fills `spender` in. */
348
+ /**
349
+ * Construction options. `kasu.flows.deposit()` fills `spender` and
350
+ * `stableAsset` in.
351
+ */
265
352
  export interface DepositFlowOptions {
266
353
  /** Agreement validity window; defaults to `CONTRACT_TTL_MS`. */
267
354
  contractTtlMs?: number;
@@ -272,6 +359,17 @@ export interface DepositFlowOptions {
272
359
  * deposit port calls.
273
360
  */
274
361
  spender?: string;
362
+ /**
363
+ * The ERC-20 the deposit is paid in — the target of the batch path's
364
+ * approve call. `kasu.flows.deposit()` passes this chain's
365
+ * `stableAsset.address`.
366
+ *
367
+ * Without it the batch path is never taken, even when `depositBatch` is
368
+ * supplied: an approve encoded against a guessed token is an approval
369
+ * granted on the wrong contract inside a transaction that then reverts.
370
+ * The run falls back to the `approve` and `deposit` ports instead.
371
+ */
372
+ stableAsset?: string;
275
373
  }
276
374
 
277
375
  // ---------------------------------------------------------------------------
@@ -315,10 +413,24 @@ export interface DepositState {
315
413
  step: DepositStep | null;
316
414
  /** 1-based badge position of `step`; `0` while idle. */
317
415
  stepIndex: number;
318
- /** `4`, or `3` when the allowance already covers the deposit. */
416
+ /**
417
+ * `4`, or `3` when the allowance already covers the deposit or the run
418
+ * is batched.
419
+ */
319
420
  stepTotal: number;
320
- /** Whether the approve step is in scope for this run. */
421
+ /**
422
+ * Whether the separate approve STEP is in scope for this run. `false`
423
+ * on a batched run too: its approval rides inside the request step's
424
+ * single transaction, so it never enters `approve` and counts as a
425
+ * three-step run on every badge.
426
+ */
321
427
  approvalRequired: boolean;
428
+ /**
429
+ * Whether this run approves and deposits as ONE atomic batch through the
430
+ * `depositBatch` port. Decided by the allowance pre-check, before the
431
+ * first signature, and `false` on every two-transaction run.
432
+ */
433
+ batched: boolean;
322
434
  /** The generated agreement, from `generating-fetch` onwards. */
323
435
  contract: GenerateContractResponse | null;
324
436
  /** Set with `phase: 'error'`, cleared by `reset()`. */
@@ -359,6 +471,7 @@ const INITIAL: DepositState = {
359
471
  stepIndex: 0,
360
472
  stepTotal: 4,
361
473
  approvalRequired: true,
474
+ batched: false,
362
475
  contract: null,
363
476
  failure: null,
364
477
  };
@@ -384,6 +497,56 @@ type AcceptOutcome =
384
497
  /** `reset()` unparked it. The run's token check drops everything after. */
385
498
  | { kind: 'abandoned' };
386
499
 
500
+ /** How the batch path's authorisation park ends. Same rules as `AcceptOutcome`. */
501
+ type AuthoriseOutcome =
502
+ | { kind: 'authorised' }
503
+ | { kind: 'declined' }
504
+ | { kind: 'abandoned' };
505
+
506
+ const ERC20 = IERC20MetadataAbi__factory.createInterface();
507
+ const LENDING_POOL_MANAGER = ILendingPoolManagerAbi__factory.createInterface();
508
+
509
+ /**
510
+ * The batch path's two calls, encoded by the SDK so no consumer has to: the
511
+ * EXACT-amount approve, then the very `requestDepositWithKyc` the default
512
+ * `deposit` port sends — `swapData` `'0x'`, no value. Both read `amount` from
513
+ * the same variable, so the batch cannot approve one figure and deposit
514
+ * another.
515
+ */
516
+ function encodeDepositBatch(args: {
517
+ stableAsset: string;
518
+ spender: string;
519
+ input: DepositFlowInput;
520
+ depositData: string;
521
+ kyc: KycSignature;
522
+ }): BatchCall[] {
523
+ const { stableAsset, spender, input, depositData, kyc } = args;
524
+ return [
525
+ {
526
+ to: stableAsset,
527
+ data: ERC20.encodeFunctionData('approve', [spender, input.amount]),
528
+ },
529
+ {
530
+ to: spender,
531
+ data: LENDING_POOL_MANAGER.encodeFunctionData(
532
+ 'requestDepositWithKyc',
533
+ [
534
+ input.poolId,
535
+ input.trancheId,
536
+ input.amount,
537
+ '0x',
538
+ input.fixedTermConfigId,
539
+ depositData,
540
+ {
541
+ blockExpiration: kyc.blockExpiration,
542
+ signature: kyc.signature,
543
+ },
544
+ ],
545
+ ),
546
+ },
547
+ ];
548
+ }
549
+
387
550
  /** 1-based badge position, with `approve` dropped when it is out of scope. */
388
551
  function stepIndexOf(step: DepositStep, approvalRequired: boolean): number {
389
552
  const order: DepositStep[] = approvalRequired
@@ -435,6 +598,23 @@ function classifyRequest(err: unknown): DepositFailure {
435
598
  return classifyWalletFailure('request', err);
436
599
  }
437
600
 
601
+ /**
602
+ * A `depositBatch` rejection. A submitter that KNOWS the batch reverts says so
603
+ * with `BatchRevertedError`; anything else is read exactly as a direct
604
+ * deposit's throw is, so a non-wallet error can never become `cancelled`.
605
+ */
606
+ function classifyBatch(err: unknown): DepositFailure {
607
+ if (err instanceof BatchRevertedError) {
608
+ return {
609
+ step: 'request',
610
+ reason: 'reverted',
611
+ revertError: err.revertError,
612
+ error: err,
613
+ };
614
+ }
615
+ return classifyRequest(err);
616
+ }
617
+
438
618
  // ---------------------------------------------------------------------------
439
619
  // The flow
440
620
  // ---------------------------------------------------------------------------
@@ -442,6 +622,7 @@ function classifyRequest(err: unknown): DepositFailure {
442
622
  export class DepositFlow extends Flow<DepositState, DepositFlowInput> {
443
623
  private readonly _ttlMs: number;
444
624
  private readonly _defaultSpender: string | undefined;
625
+ private readonly _stableAsset: string | undefined;
445
626
  private readonly _now: () => number;
446
627
 
447
628
  /**
@@ -464,6 +645,15 @@ export class DepositFlow extends Flow<DepositState, DepositFlowInput> {
464
645
  */
465
646
  private _acceptingFor: number | null = null;
466
647
 
648
+ /**
649
+ * The batch path's authorisation handshake. Same shape and lifetime as
650
+ * `_accept`: the run parks on it, `authoriseTransfer`, `declineTransfer`
651
+ * and `reset` settle it, and it is cleared the moment it settles.
652
+ */
653
+ private _authorise: {
654
+ settle: (outcome: AuthoriseOutcome) => void;
655
+ } | null = null;
656
+
467
657
  constructor(
468
658
  private readonly _ports: DepositPorts,
469
659
  opts?: DepositFlowOptions,
@@ -471,6 +661,7 @@ export class DepositFlow extends Flow<DepositState, DepositFlowInput> {
471
661
  super(INITIAL);
472
662
  this._ttlMs = opts?.contractTtlMs ?? CONTRACT_TTL_MS;
473
663
  this._defaultSpender = opts?.spender;
664
+ this._stableAsset = opts?.stableAsset;
474
665
  // Called through the ports object, never captured off it: a consumer
475
666
  // whose clock is a method on its own object keeps its `this`.
476
667
  this._now = (): number => _ports.now?.() ?? Date.now();
@@ -536,6 +727,33 @@ export class DepositFlow extends Flow<DepositState, DepositFlowInput> {
536
727
  bridge.settle({ kind: 'declined' });
537
728
  }
538
729
 
730
+ /**
731
+ * Batch path: authorise the single transaction that approves and deposits,
732
+ * and resume the parked run. A no-op when nothing is parked on
733
+ * `awaiting-authorise`, so a double tap cannot submit twice.
734
+ *
735
+ * The batch has no wallet prompt per call, so this is the lender's
736
+ * explicit consent to the transfer — call it only from their action.
737
+ */
738
+ authoriseTransfer(): void {
739
+ const bridge = this._authorise;
740
+ if (!bridge) return;
741
+ this._authorise = null;
742
+ bridge.settle({ kind: 'authorised' });
743
+ }
744
+
745
+ /**
746
+ * Batch path: refuse the transfer. The run ends on `error` with `{ step:
747
+ * 'request', reason: 'cancelled' }` and nothing is submitted. A no-op when
748
+ * nothing is parked on `awaiting-authorise`.
749
+ */
750
+ declineTransfer(): void {
751
+ const bridge = this._authorise;
752
+ if (!bridge) return;
753
+ this._authorise = null;
754
+ bridge.settle({ kind: 'declined' });
755
+ }
756
+
539
757
  /** True only while THIS generation is waiting on the acceptance signature. */
540
758
  private _isAccepting(): boolean {
541
759
  return (
@@ -550,6 +768,9 @@ export class DepositFlow extends Flow<DepositState, DepositFlowInput> {
550
768
  this._accept = null;
551
769
  this._acceptingFor = null;
552
770
  bridge?.settle({ kind: 'abandoned' });
771
+ const authorise = this._authorise;
772
+ this._authorise = null;
773
+ authorise?.settle({ kind: 'abandoned' });
553
774
  }
554
775
 
555
776
  // -----------------------------------------------------------------------
@@ -560,6 +781,7 @@ export class DepositFlow extends Flow<DepositState, DepositFlowInput> {
560
781
  ): Promise<void> {
561
782
  this._accept = null;
562
783
  this._acceptingFor = null;
784
+ this._authorise = null;
563
785
  const ports = this._ports;
564
786
  const owner = input.userAddress.toLowerCase();
565
787
 
@@ -582,22 +804,35 @@ export class DepositFlow extends Flow<DepositState, DepositFlowInput> {
582
804
  // live, never cached: an exact-amount approval is fully consumed by
583
805
  // the deposit it paid for, so a stale allowance is exactly the value
584
806
  // that would wrongly skip the approve and revert the deposit.
585
- let approvalRequired = true;
807
+ let allowanceShort = true;
586
808
  try {
587
809
  const allowance = await ports.readAllowance(owner, spender);
588
- approvalRequired = allowance.lt(input.amount);
810
+ allowanceShort = allowance.lt(input.amount);
589
811
  } catch {
590
812
  // A read failure is not a reason to skip an approval. Assume one is
591
813
  // needed: the cost is a redundant approve, the alternative is a
592
814
  // reverted deposit.
593
- approvalRequired = true;
815
+ allowanceShort = true;
594
816
  }
595
817
  if (!this._store.isCurrent(token)) return;
596
818
 
819
+ // The path is chosen HERE, with the allowance, so the badge is right
820
+ // from the first state the lender sees. Batch only when there is an
821
+ // approval to fold in, a port to submit it, and a known token to
822
+ // approve on — never a guessed one. A batched run never enters the
823
+ // `approve` step, so every badge counts it as a three-step run.
824
+ const stableAsset = this._stableAsset;
825
+ const batched =
826
+ allowanceShort &&
827
+ typeof ports.depositBatch === 'function' &&
828
+ !!stableAsset;
829
+ const approvalRequired = allowanceShort && !batched;
830
+
597
831
  // 2. Generate — sign the auth message.
598
832
  this._store.patch(
599
833
  {
600
834
  approvalRequired,
835
+ batched,
601
836
  phase: 'generating-sign',
602
837
  ...badgeFor('generate', approvalRequired),
603
838
  },
@@ -736,6 +971,19 @@ export class DepositFlow extends Flow<DepositState, DepositFlowInput> {
736
971
  return;
737
972
  }
738
973
 
974
+ if (batched && stableAsset) {
975
+ await this._runBatch({
976
+ token,
977
+ input,
978
+ owner,
979
+ spender,
980
+ stableAsset,
981
+ depositData,
982
+ contract,
983
+ });
984
+ return;
985
+ }
986
+
739
987
  // 6. Approve — the EXACT amount, never `MaxUint256`. House rule: an
740
988
  // unlimited allowance outlives the deposit it was granted for, and a
741
989
  // later exploit of the spender would drain a wallet that has long
@@ -818,6 +1066,136 @@ export class DepositFlow extends Flow<DepositState, DepositFlowInput> {
818
1066
  );
819
1067
  }
820
1068
 
1069
+ /**
1070
+ * The batch path, from the accepted agreement to the receipt.
1071
+ *
1072
+ * 1. PARK on `awaiting-authorise`. A batch puts no wallet prompt in front
1073
+ * of each call, so the flow waits for an explicit authorisation before
1074
+ * any money moves. `declineTransfer()` ends it as `cancelled`.
1075
+ * 2. Re-check the TTL: the lender may have idled on the authorisation as
1076
+ * long as they did on the agreement.
1077
+ * 3. Only now fetch the KYC signature — it has a block expiration of its
1078
+ * own, and an authorisation left open must not spend it.
1079
+ * 4. Encode both calls and hand them to `depositBatch`; then the receipt.
1080
+ */
1081
+ private async _runBatch(args: {
1082
+ token: number;
1083
+ input: DepositFlowInput;
1084
+ owner: string;
1085
+ spender: string;
1086
+ stableAsset: string;
1087
+ depositData: string;
1088
+ contract: GenerateContractResponse;
1089
+ }): Promise<void> {
1090
+ const { token, input, owner, spender, stableAsset, depositData } =
1091
+ args;
1092
+ const ports = this._ports;
1093
+ // A batched run is a three-step run on every badge.
1094
+ const approvalRequired = false;
1095
+
1096
+ // Every settler clears the handshake before settling it, so there is
1097
+ // nothing to clear here — and clearing it anyway could drop the
1098
+ // handshake of a NEWER run that `reset(); start()` already parked.
1099
+ const outcome = await new Promise<AuthoriseOutcome>((resolve) => {
1100
+ this._authorise = { settle: resolve };
1101
+ this._store.patch(
1102
+ {
1103
+ phase: 'awaiting-authorise',
1104
+ ...badgeFor('request', approvalRequired),
1105
+ },
1106
+ token,
1107
+ );
1108
+ });
1109
+ if (!this._store.isCurrent(token)) return;
1110
+ if (outcome.kind === 'declined') {
1111
+ this._fail(token, approvalRequired, {
1112
+ step: 'request',
1113
+ reason: 'cancelled',
1114
+ });
1115
+ return;
1116
+ }
1117
+ if (outcome.kind !== 'authorised') return;
1118
+
1119
+ if (this._now() > args.contract.timestamp + this._ttlMs) {
1120
+ this._fail(token, approvalRequired, {
1121
+ step: 'request',
1122
+ reason: 'contract-expired',
1123
+ });
1124
+ return;
1125
+ }
1126
+
1127
+ this._store.patch(
1128
+ { phase: 'request-sign', ...badgeFor('request', approvalRequired) },
1129
+ token,
1130
+ );
1131
+
1132
+ // The consumer's backend, so `failed` — never `cancelled` — exactly as
1133
+ // on the two-transaction path.
1134
+ let kyc: KycSignature;
1135
+ try {
1136
+ const kycParams = await ports.buildKycParams(
1137
+ owner as `0x${string}`,
1138
+ );
1139
+ kyc = await ports.getKycSignature(kycParams);
1140
+ } catch (err) {
1141
+ this._fail(token, approvalRequired, {
1142
+ step: 'request',
1143
+ reason: 'failed',
1144
+ error: err,
1145
+ });
1146
+ return;
1147
+ }
1148
+ if (!this._store.isCurrent(token)) return;
1149
+
1150
+ // Encoded from values a backend returned (the KYC signature), so a
1151
+ // shape the coder refuses is a failure the consumer can render, not a
1152
+ // rejection out of a `start()` that promises it has none.
1153
+ let calls: BatchCall[];
1154
+ try {
1155
+ calls = encodeDepositBatch({
1156
+ stableAsset,
1157
+ spender,
1158
+ input,
1159
+ depositData,
1160
+ kyc,
1161
+ });
1162
+ } catch (err) {
1163
+ this._fail(token, approvalRequired, {
1164
+ step: 'request',
1165
+ reason: 'failed',
1166
+ error: err,
1167
+ });
1168
+ return;
1169
+ }
1170
+
1171
+ try {
1172
+ // Called through the ports object, so a port that is a method
1173
+ // keeps its `this`. Its presence decided the path; a consumer
1174
+ // that removed it since gets a `failed`, not a crash.
1175
+ if (typeof ports.depositBatch !== 'function') {
1176
+ throw new Error('DepositFlow: the depositBatch port is gone');
1177
+ }
1178
+ const tx = await ports.depositBatch(calls);
1179
+ this._store.patch(
1180
+ {
1181
+ phase: 'request-confirm',
1182
+ ...badgeFor('request', approvalRequired),
1183
+ },
1184
+ token,
1185
+ );
1186
+ await tx.wait();
1187
+ } catch (err) {
1188
+ this._fail(token, approvalRequired, classifyBatch(err));
1189
+ return;
1190
+ }
1191
+ if (!this._store.isCurrent(token)) return;
1192
+
1193
+ this._store.patch(
1194
+ { phase: 'success', ...badgeFor('request', approvalRequired) },
1195
+ token,
1196
+ );
1197
+ }
1198
+
821
1199
  private _fail(
822
1200
  token: number,
823
1201
  approvalRequired: boolean,
@@ -15,6 +15,7 @@ export { FlowStore } from './observable';
15
15
  export type { WaitableTransaction } from './observable';
16
16
 
17
17
  export {
18
+ BatchRevertedError,
18
19
  CONTRACT_TTL_MS,
19
20
  DepositFlow,
20
21
  INVALID_ACCEPTANCE_SIGNATURE_MESSAGE,
@@ -22,6 +23,7 @@ export {
22
23
  NO_SPENDER_MESSAGE,
23
24
  } from './deposit-flow';
24
25
  export type {
26
+ BatchCall,
25
27
  ContractMessageRequest,
26
28
  DepositFailure,
27
29
  DepositFlowInput,