@kasufinance/kasu-sdk 2.8.0 → 2.10.1
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 +120 -9
- package/dist/bundle.cjs.js +2077 -476
- package/dist/bundle.esm.js +2039 -478
- package/dist/domain/batch-proof.d.ts +210 -0
- package/dist/domain/batch-proof.js +384 -0
- package/dist/domain/batch-proof.js.map +1 -0
- package/dist/domain/explorer-urls.d.ts +33 -0
- package/dist/domain/explorer-urls.js +38 -0
- package/dist/domain/explorer-urls.js.map +1 -0
- package/dist/domain/fixed-term.d.ts +74 -0
- package/dist/domain/fixed-term.js +86 -0
- package/dist/domain/fixed-term.js.map +1 -0
- package/dist/domain/index.d.ts +12 -2
- package/dist/domain/index.js +7 -2
- package/dist/domain/index.js.map +1 -1
- package/dist/domain/partners.d.ts +29 -0
- package/dist/domain/partners.js +20 -0
- package/dist/domain/partners.js.map +1 -1
- package/dist/domain/requests.d.ts +24 -0
- package/dist/domain/requests.js +33 -0
- package/dist/domain/requests.js.map +1 -1
- package/dist/domain/sponsored-calls.d.ts +74 -0
- package/dist/domain/sponsored-calls.js +156 -0
- package/dist/domain/sponsored-calls.js.map +1 -0
- package/dist/domain/yields.d.ts +121 -0
- package/dist/domain/yields.js +151 -0
- package/dist/domain/yields.js.map +1 -0
- package/dist/facade/calls.d.ts +59 -0
- package/dist/facade/calls.js +89 -0
- package/dist/facade/calls.js.map +1 -0
- package/dist/facade/chain-configs.js +5 -0
- package/dist/facade/chain-configs.js.map +1 -1
- package/dist/facade/flows.d.ts +24 -3
- package/dist/facade/flows.js +22 -2
- package/dist/facade/flows.js.map +1 -1
- package/dist/facade/index.d.ts +3 -1
- package/dist/facade/index.js +1 -0
- package/dist/facade/index.js.map +1 -1
- package/dist/facade/kasu.d.ts +8 -2
- package/dist/facade/kasu.js +17 -5
- package/dist/facade/kasu.js.map +1 -1
- package/dist/facade/strategies.d.ts +43 -2
- package/dist/facade/strategies.js +80 -4
- package/dist/facade/strategies.js.map +1 -1
- package/dist/facade/types.d.ts +18 -0
- package/dist/flows/batch-errors.d.ts +74 -0
- package/dist/flows/batch-errors.js +83 -0
- package/dist/flows/batch-errors.js.map +1 -0
- package/dist/flows/batch-submission.d.ts +223 -0
- package/dist/flows/batch-submission.js +321 -0
- package/dist/flows/batch-submission.js.map +1 -0
- package/dist/flows/deposit-flow.d.ts +13 -20
- package/dist/flows/deposit-flow.js +14 -25
- package/dist/flows/deposit-flow.js.map +1 -1
- package/dist/flows/index.d.ts +6 -2
- package/dist/flows/index.js +4 -2
- package/dist/flows/index.js.map +1 -1
- package/dist/flows/withdraw-flow.d.ts +95 -4
- package/dist/flows/withdraw-flow.js +107 -2
- package/dist/flows/withdraw-flow.js.map +1 -1
- package/dist/services/DataService/queries.js +1 -0
- package/dist/services/DataService/queries.js.map +1 -1
- package/dist/services/Portfolio/portfolio.js +11 -0
- package/dist/services/Portfolio/portfolio.js.map +1 -1
- package/dist/services/UserLending/user-lending.d.ts +13 -0
- package/dist/services/UserLending/user-lending.js +25 -4
- package/dist/services/UserLending/user-lending.js.map +1 -1
- package/package.json +1 -1
- package/src/domain/batch-proof.ts +576 -0
- package/src/domain/explorer-urls.ts +74 -0
- package/src/domain/fixed-term.ts +115 -0
- package/src/domain/index.ts +56 -0
- package/src/domain/partners.ts +35 -0
- package/src/domain/requests.ts +37 -0
- package/src/domain/sponsored-calls.ts +213 -0
- package/src/domain/yields.ts +189 -0
- package/src/facade/calls.ts +135 -0
- package/src/facade/chain-configs.ts +5 -0
- package/src/facade/flows.ts +23 -2
- package/src/facade/index.ts +3 -0
- package/src/facade/kasu.ts +31 -2
- package/src/facade/strategies.ts +98 -7
- package/src/facade/types.ts +19 -0
- package/src/flows/batch-errors.ts +118 -0
- package/src/flows/batch-submission.ts +511 -0
- package/src/flows/deposit-flow.ts +27 -33
- package/src/flows/index.ts +33 -2
- package/src/flows/withdraw-flow.ts +219 -10
- package/src/services/DataService/queries.ts +1 -0
- package/src/services/Portfolio/portfolio.ts +14 -0
- package/src/services/UserLending/user-lending.ts +38 -10
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
} from '../domain/wallet-errors';
|
|
17
17
|
import { DepositParams, KycParams } from '../facade/types';
|
|
18
18
|
|
|
19
|
+
import { BatchRevertedError, BatchSubmissionError } from './batch-errors';
|
|
19
20
|
import { Flow } from './flow';
|
|
20
21
|
import { WaitableTransaction } from './observable';
|
|
21
22
|
|
|
@@ -167,7 +168,19 @@ export type DepositFailure =
|
|
|
167
168
|
revertError: string;
|
|
168
169
|
error: unknown;
|
|
169
170
|
}
|
|
170
|
-
| { step: 'request'; reason: 'contract-expired' }
|
|
171
|
+
| { step: 'request'; reason: 'contract-expired' }
|
|
172
|
+
| {
|
|
173
|
+
step: 'request';
|
|
174
|
+
/**
|
|
175
|
+
* Batch path only — a `BatchSubmissionError` whose outcome is not
|
|
176
|
+
* `failed`. `pending`: the batch was, or may have been, accepted and
|
|
177
|
+
* nothing proves whether it deposited — NEVER render it as a
|
|
178
|
+
* failure, which invites a second deposit. `already-submitted`: an
|
|
179
|
+
* earlier submission was proven, so this one was refused.
|
|
180
|
+
*/
|
|
181
|
+
reason: 'pending' | 'already-submitted';
|
|
182
|
+
error: unknown;
|
|
183
|
+
};
|
|
171
184
|
|
|
172
185
|
// ---------------------------------------------------------------------------
|
|
173
186
|
// Ports
|
|
@@ -184,38 +197,9 @@ export interface BatchCall {
|
|
|
184
197
|
data: string;
|
|
185
198
|
}
|
|
186
199
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
}
|
|
200
|
+
// `BatchRevertedError` lives in `./batch-errors` beside the submission
|
|
201
|
+
// errors, and is re-exported here so every existing import keeps working.
|
|
202
|
+
export { BatchRevertedError };
|
|
219
203
|
|
|
220
204
|
/** What the KYC signing service hands back. */
|
|
221
205
|
export interface KycSignature {
|
|
@@ -604,6 +588,16 @@ function classifyRequest(err: unknown): DepositFailure {
|
|
|
604
588
|
* deposit's throw is, so a non-wallet error can never become `cancelled`.
|
|
605
589
|
*/
|
|
606
590
|
function classifyBatch(err: unknown): DepositFailure {
|
|
591
|
+
// A submission outcome is read FIRST and taken at its word. It must never
|
|
592
|
+
// reach the revert-data walk below: that walk follows `cause`, and an
|
|
593
|
+
// ambiguous outcome whose cause happens to carry revert bytes would read
|
|
594
|
+
// as `reverted` or `insufficient-balance` — "nothing was deposited, try
|
|
595
|
+
// again" — on a batch that may have moved the money.
|
|
596
|
+
if (err instanceof BatchSubmissionError) {
|
|
597
|
+
return err.outcome === 'failed'
|
|
598
|
+
? { step: 'request', reason: 'failed', error: err }
|
|
599
|
+
: { step: 'request', reason: err.outcome, error: err };
|
|
600
|
+
}
|
|
607
601
|
if (err instanceof BatchRevertedError) {
|
|
608
602
|
return {
|
|
609
603
|
step: 'request',
|
package/src/flows/index.ts
CHANGED
|
@@ -10,12 +10,38 @@
|
|
|
10
10
|
* Every application drove its own copy of the deposit pipeline before this, and
|
|
11
11
|
* the copies had already started to differ.
|
|
12
12
|
*/
|
|
13
|
+
export {
|
|
14
|
+
BatchPendingError,
|
|
15
|
+
BatchRevertedError,
|
|
16
|
+
batchOutcomeOf,
|
|
17
|
+
BatchSubmissionError,
|
|
18
|
+
isDefinitivelyNotSubmitted,
|
|
19
|
+
} from './batch-errors';
|
|
20
|
+
export type { BatchSubmissionOutcome } from './batch-errors';
|
|
21
|
+
|
|
22
|
+
export {
|
|
23
|
+
checkBatchOutcome,
|
|
24
|
+
DEFAULT_BATCH_POLLING_POLICY,
|
|
25
|
+
submitBatch,
|
|
26
|
+
submitSponsoredCall,
|
|
27
|
+
} from './batch-submission';
|
|
28
|
+
export type {
|
|
29
|
+
BatchPollingPolicy,
|
|
30
|
+
BatchStatusRead,
|
|
31
|
+
BatchSubmission,
|
|
32
|
+
BatchSubmissionPorts,
|
|
33
|
+
ExecutedCall,
|
|
34
|
+
ProvenBatch,
|
|
35
|
+
SponsoredCallSubmission,
|
|
36
|
+
SponsoredCallSubmissionPorts,
|
|
37
|
+
SubmitBatchCall,
|
|
38
|
+
} from './batch-submission';
|
|
39
|
+
|
|
13
40
|
export { Flow } from './flow';
|
|
14
41
|
export { FlowStore } from './observable';
|
|
15
42
|
export type { WaitableTransaction } from './observable';
|
|
16
43
|
|
|
17
44
|
export {
|
|
18
|
-
BatchRevertedError,
|
|
19
45
|
CONTRACT_TTL_MS,
|
|
20
46
|
DepositFlow,
|
|
21
47
|
INVALID_ACCEPTANCE_SIGNATURE_MESSAGE,
|
|
@@ -38,7 +64,12 @@ export type {
|
|
|
38
64
|
LoanAgreementRequest,
|
|
39
65
|
} from './deposit-flow';
|
|
40
66
|
|
|
41
|
-
export {
|
|
67
|
+
export {
|
|
68
|
+
NO_KYC_PARAMS_MESSAGE,
|
|
69
|
+
NO_WITHDRAWAL_ENCODER_MESSAGE,
|
|
70
|
+
NOT_A_WITHDRAWAL_CALL_MESSAGE,
|
|
71
|
+
WithdrawFlow,
|
|
72
|
+
} from './withdraw-flow';
|
|
42
73
|
export type {
|
|
43
74
|
WithdrawFailure,
|
|
44
75
|
WithdrawFlowInput,
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { BigNumber } from 'ethers';
|
|
2
2
|
|
|
3
|
-
import {
|
|
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
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
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(
|
|
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
|
}
|
|
@@ -113,6 +113,20 @@ export class Portfolio {
|
|
|
113
113
|
investedAmount * parseFloat(tranche.interestRate);
|
|
114
114
|
|
|
115
115
|
for (const fixedDeposits of tranche.fixedLoans) {
|
|
116
|
+
// A matured lock's principal is already inside
|
|
117
|
+
// `investedAmount`: getPortfolioLendingData subtracts only
|
|
118
|
+
// the LOCKED loans from the tranche balance. Its lifetime
|
|
119
|
+
// yield is earned and stays counted (the tranche's own
|
|
120
|
+
// lifetime had every lock's yield taken out), but its
|
|
121
|
+
// amount must not be added to the investment, weekly or
|
|
122
|
+
// APY sums a second time.
|
|
123
|
+
if (!fixedDeposits.isLocked) {
|
|
124
|
+
totalYieldEarned += parseFloat(
|
|
125
|
+
fixedDeposits.yieldEarnings.lifetime,
|
|
126
|
+
);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
116
130
|
const ftdConfig = tranche.fixedTermConfig.find(
|
|
117
131
|
(ftd) => ftd.configId === fixedDeposits.configId,
|
|
118
132
|
);
|
|
@@ -278,17 +278,50 @@ export class UserLending {
|
|
|
278
278
|
);
|
|
279
279
|
}
|
|
280
280
|
|
|
281
|
-
|
|
282
|
-
|
|
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<
|
|
290
|
+
): Promise<BigNumber> {
|
|
286
291
|
const trancheContract: ILendingPoolTrancheAbi =
|
|
287
292
|
ILendingPoolTrancheAbi__factory.connect(
|
|
288
293
|
tranche,
|
|
289
294
|
this._signerOrProvider,
|
|
290
295
|
);
|
|
291
|
-
|
|
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
|
|
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,
|