@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 +45 -1
- package/dist/bundle.cjs.js +260 -7
- package/dist/bundle.esm.js +260 -8
- package/dist/facade/flows.d.ts +17 -1
- package/dist/facade/flows.js +15 -2
- package/dist/facade/flows.js.map +1 -1
- package/dist/facade/kasu.js +2 -1
- package/dist/facade/kasu.js.map +1 -1
- package/dist/flows/deposit-flow.d.ts +135 -4
- package/dist/flows/deposit-flow.js +244 -4
- package/dist/flows/deposit-flow.js.map +1 -1
- package/dist/flows/index.d.ts +2 -2
- package/dist/flows/index.js +1 -1
- package/dist/flows/index.js.map +1 -1
- package/package.json +1 -1
- package/src/facade/flows.ts +12 -0
- package/src/facade/kasu.ts +1 -0
- package/src/flows/deposit-flow.ts +384 -6
- package/src/flows/index.ts +2 -0
package/src/facade/flows.ts
CHANGED
|
@@ -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
|
}
|
package/src/facade/kasu.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
/**
|
|
416
|
+
/**
|
|
417
|
+
* `4`, or `3` when the allowance already covers the deposit or the run
|
|
418
|
+
* is batched.
|
|
419
|
+
*/
|
|
319
420
|
stepTotal: number;
|
|
320
|
-
/**
|
|
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
|
|
807
|
+
let allowanceShort = true;
|
|
586
808
|
try {
|
|
587
809
|
const allowance = await ports.readAllowance(owner, spender);
|
|
588
|
-
|
|
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
|
-
|
|
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,
|
package/src/flows/index.ts
CHANGED
|
@@ -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,
|