@kasufinance/kasu-sdk 2.5.0 → 2.7.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 +144 -7
- package/dist/bundle.cjs.js +1667 -21
- package/dist/bundle.esm.js +1630 -22
- package/dist/domain/au-minimum.d.ts +135 -0
- package/dist/domain/au-minimum.js +154 -0
- package/dist/domain/au-minimum.js.map +1 -0
- package/dist/domain/index.d.ts +18 -3
- package/dist/domain/index.js +13 -3
- package/dist/domain/index.js.map +1 -1
- package/dist/domain/loan-contract.d.ts +174 -0
- package/dist/domain/loan-contract.js +160 -0
- package/dist/domain/loan-contract.js.map +1 -0
- package/dist/domain/requests.d.ts +219 -0
- package/dist/domain/requests.js +218 -0
- package/dist/domain/requests.js.map +1 -0
- package/dist/domain/settlement.d.ts +97 -0
- package/dist/domain/settlement.js +117 -0
- package/dist/domain/settlement.js.map +1 -0
- package/dist/domain/wallet-errors.d.ts +92 -0
- package/dist/domain/wallet-errors.js +155 -0
- package/dist/domain/wallet-errors.js.map +1 -0
- package/dist/facade/chain-configs.js +7 -1
- package/dist/facade/chain-configs.js.map +1 -1
- package/dist/facade/deposits.js +2 -1
- package/dist/facade/deposits.js.map +1 -1
- package/dist/facade/flows.d.ts +95 -0
- package/dist/facade/flows.js +116 -0
- package/dist/facade/flows.js.map +1 -0
- package/dist/facade/index.d.ts +3 -0
- package/dist/facade/index.js +3 -0
- package/dist/facade/index.js.map +1 -1
- package/dist/facade/kasu.d.ts +6 -2
- package/dist/facade/kasu.js +28 -3
- package/dist/facade/kasu.js.map +1 -1
- package/dist/facade/read-only.d.ts +12 -0
- package/dist/facade/read-only.js +13 -0
- package/dist/facade/read-only.js.map +1 -0
- package/dist/facade/user-portfolio.d.ts +18 -0
- package/dist/facade/user-portfolio.js +23 -0
- package/dist/facade/user-portfolio.js.map +1 -1
- package/dist/flows/deposit-flow.d.ts +301 -0
- package/dist/flows/deposit-flow.js +358 -0
- package/dist/flows/deposit-flow.js.map +1 -0
- package/dist/flows/flow.d.ts +72 -0
- package/dist/flows/flow.js +110 -0
- package/dist/flows/flow.js.map +1 -0
- package/dist/flows/index.d.ts +19 -0
- package/dist/flows/index.js +17 -0
- package/dist/flows/index.js.map +1 -0
- package/dist/flows/observable.d.ts +59 -0
- package/dist/flows/observable.js +81 -0
- package/dist/flows/observable.js.map +1 -0
- package/dist/flows/withdraw-flow.d.ts +99 -0
- package/dist/flows/withdraw-flow.js +83 -0
- package/dist/flows/withdraw-flow.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/services/DataService/data-service.js +3 -11
- package/dist/services/DataService/data-service.js.map +1 -1
- package/dist/services/DataService/directus-client.d.ts +26 -0
- package/dist/services/DataService/directus-client.js +38 -0
- package/dist/services/DataService/directus-client.js.map +1 -0
- package/dist/services/UserLending/user-lending.js +11 -7
- package/dist/services/UserLending/user-lending.js.map +1 -1
- package/package.json +8 -2
- package/src/domain/au-minimum.ts +192 -0
- package/src/domain/index.ts +73 -3
- package/src/domain/loan-contract.ts +275 -0
- package/src/domain/requests.ts +467 -0
- package/src/domain/settlement.ts +161 -0
- package/src/domain/wallet-errors.ts +173 -0
- package/src/facade/chain-configs.ts +7 -1
- package/src/facade/deposits.ts +2 -3
- package/src/facade/flows.ts +172 -0
- package/src/facade/index.ts +8 -0
- package/src/facade/kasu.ts +43 -3
- package/src/facade/read-only.ts +13 -0
- package/src/facade/user-portfolio.ts +24 -0
- package/src/flows/deposit-flow.ts +775 -0
- package/src/flows/flow.ts +108 -0
- package/src/flows/index.ts +45 -0
- package/src/flows/observable.ts +97 -0
- package/src/flows/withdraw-flow.ts +210 -0
- package/src/index.ts +9 -0
- package/src/services/DataService/data-service.ts +7 -25
- package/src/services/DataService/directus-client.ts +54 -0
- package/src/services/UserLending/user-lending.ts +17 -21
- package/dist/domain/deposit-bounds.test.d.ts +0 -1
- package/dist/domain/deposit-bounds.test.js +0 -135
- package/dist/domain/deposit-bounds.test.js.map +0 -1
- package/dist/domain/partners.test.d.ts +0 -1
- package/dist/domain/partners.test.js +0 -53
- package/dist/domain/partners.test.js.map +0 -1
- package/dist/domain/pools.test.d.ts +0 -1
- package/dist/domain/pools.test.js +0 -184
- package/dist/domain/pools.test.js.map +0 -1
- package/dist/domain/rates.test.d.ts +0 -1
- package/dist/domain/rates.test.js +0 -181
- package/dist/domain/rates.test.js.map +0 -1
- package/dist/domain/tranche-display-name.test.d.ts +0 -1
- package/dist/domain/tranche-display-name.test.js +0 -58
- package/dist/domain/tranche-display-name.test.js.map +0 -1
- package/dist/domain/tranches.test.d.ts +0 -1
- package/dist/domain/tranches.test.js +0 -206
- package/dist/domain/tranches.test.js.map +0 -1
- package/dist/facade/config.test.d.ts +0 -1
- package/dist/facade/config.test.js +0 -216
- package/dist/facade/config.test.js.map +0 -1
- package/dist/facade/facade.test.d.ts +0 -1
- package/dist/facade/facade.test.js +0 -201
- package/dist/facade/facade.test.js.map +0 -1
- package/dist/services/Locking/calculate-apy.test.d.ts +0 -1
- package/dist/services/Locking/calculate-apy.test.js +0 -41
- package/dist/services/Locking/calculate-apy.test.js.map +0 -1
- package/dist/tests/sample.test.d.ts +0 -1
- package/dist/tests/sample.test.js +0 -59
- package/dist/tests/sample.test.js.map +0 -1
- package/src/domain/deposit-bounds.test.ts +0 -200
- package/src/domain/partners.test.ts +0 -83
- package/src/domain/pools.test.ts +0 -260
- package/src/domain/rates.test.ts +0 -254
- package/src/domain/tranche-display-name.test.ts +0 -96
- package/src/domain/tranches.test.ts +0 -300
- package/src/facade/config.test.ts +0 -265
- package/src/facade/facade.test.ts +0 -250
- package/src/services/Locking/calculate-apy.test.ts +0 -52
- package/src/tests/sample.test.ts +0 -63
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
import { UserRequestStatus } from '../services/UserLending/subgraph-types';
|
|
2
|
+
import {
|
|
3
|
+
UserRequest,
|
|
4
|
+
UserRequestEvent,
|
|
5
|
+
} from '../services/UserLending/types';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The lending-request view model, as CODES.
|
|
9
|
+
*
|
|
10
|
+
* Lifted verbatim from kasu-ui's `derive-transaction-view.ts` +
|
|
11
|
+
* `request-bundle.ts`, minus every word. kasu-ui's `deriveTransactionView`
|
|
12
|
+
* produces a `TransactionView` that mixes derived FACTS (status code, amounts,
|
|
13
|
+
* kind, ids, submission count, timestamps) with COPY (the status word, the
|
|
14
|
+
* type label, the detail line beneath it, tooltip keys). Only the facts belong
|
|
15
|
+
* in this layer: the copy stays in each application, where the design system,
|
|
16
|
+
* the register and the visitor's language are.
|
|
17
|
+
*
|
|
18
|
+
* The branch ORDER below is the behaviour — cancelled beats forced beats
|
|
19
|
+
* reallocated beats a live withdrawal remainder beats the resolved outcomes
|
|
20
|
+
* beats pending. It reproduces `deriveTransactionView` check for check, so a
|
|
21
|
+
* consumer that renders its own words on top of `statusCode` gets exactly the
|
|
22
|
+
* row kasu-ui has been showing.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Stable status code for filters, tests and the caller's own status word.
|
|
27
|
+
* Independent of any user-facing copy — kasu-ui's four-value vocabulary
|
|
28
|
+
* (Queued / Processing / Completed / Cancelled) is a rendering of these seven
|
|
29
|
+
* codes, not a replacement for them.
|
|
30
|
+
*/
|
|
31
|
+
export type RequestStatusCode =
|
|
32
|
+
| 'pending'
|
|
33
|
+
| 'complete'
|
|
34
|
+
| 'partial'
|
|
35
|
+
| 'rejected'
|
|
36
|
+
| 'reallocated'
|
|
37
|
+
| 'cancelled'
|
|
38
|
+
| 'forced';
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Direction of value flow into / out of the lender's balance. `neutral`
|
|
42
|
+
* applies when the request was cancelled or fully rejected — nothing moved.
|
|
43
|
+
*/
|
|
44
|
+
export type RequestKind = 'inflow' | 'outflow' | 'neutral';
|
|
45
|
+
|
|
46
|
+
/** One derived request row. Every field is a number, a code or an id. */
|
|
47
|
+
export interface RequestState {
|
|
48
|
+
/** `UserRequest.id`. */
|
|
49
|
+
id: string;
|
|
50
|
+
/**
|
|
51
|
+
* Id the loan contract is retrieved by — sourced from the `Initiated`
|
|
52
|
+
* event (format `${requestId}-${index}`). The agreements upstream keys
|
|
53
|
+
* contract metadata by EVENT id, not request id; passing `id` returns
|
|
54
|
+
* "Signature data not found". Empty string when there is no `Initiated`
|
|
55
|
+
* event (e.g. a withdrawal).
|
|
56
|
+
*/
|
|
57
|
+
contractId: string;
|
|
58
|
+
/** Pool address, lowercased — the filter/join key. */
|
|
59
|
+
poolId: string;
|
|
60
|
+
/**
|
|
61
|
+
* Pool name exactly as it arrived. RAW: splitting it into a strategy name
|
|
62
|
+
* and its borrower-type subheading (kasu-ui's `splitPoolName`) is a
|
|
63
|
+
* display decision and stays in the application.
|
|
64
|
+
*/
|
|
65
|
+
poolName: string;
|
|
66
|
+
/**
|
|
67
|
+
* Tranche name exactly as the subgraph reports it. RAW: apps call
|
|
68
|
+
* `getTrancheDisplayName(state.trancheName, { poolName: state.poolName })`
|
|
69
|
+
* at the view boundary. A renamed value must never reach matching or
|
|
70
|
+
* ranking code.
|
|
71
|
+
*/
|
|
72
|
+
trancheName: string;
|
|
73
|
+
/** Tranche id — the resolve API re-renders a fixed-term contract against it. */
|
|
74
|
+
trancheId: string;
|
|
75
|
+
/** Fixed-term configId, `'0'` for a variable-rate request. */
|
|
76
|
+
fixedTermConfigId: string;
|
|
77
|
+
requestType: 'Deposit' | 'Withdrawal';
|
|
78
|
+
/** The subgraph status, untouched. Sticky — see `cycleClosed`. */
|
|
79
|
+
rawStatus: UserRequest['status'];
|
|
80
|
+
statusCode: RequestStatusCode;
|
|
81
|
+
kind: RequestKind;
|
|
82
|
+
/**
|
|
83
|
+
* The amount originally asked for, always positive. A cancelled request
|
|
84
|
+
* has its on-request `requestedAmount` zeroed by the subgraph, so the
|
|
85
|
+
* original is recovered from the `Initiated` event (see
|
|
86
|
+
* `initiatedAmount`).
|
|
87
|
+
*/
|
|
88
|
+
requestedAmount: number;
|
|
89
|
+
/**
|
|
90
|
+
* The accepted figure as the subgraph reported it, or `null` when it
|
|
91
|
+
* reported none. `null` is NOT zero: an absent figure means "not known",
|
|
92
|
+
* and a caller that renders it as 0 states an outcome the chain has not
|
|
93
|
+
* given. The branch logic below reads `null` as 0 — exactly what kasu-ui's
|
|
94
|
+
* `num()` does — but the distinction survives into the row.
|
|
95
|
+
*/
|
|
96
|
+
acceptedAmount: number | null;
|
|
97
|
+
/**
|
|
98
|
+
* `assetAmount` of the `Initiated` event, or `null` when the request
|
|
99
|
+
* carries no `Initiated` event. This is the cancelled-amount recovery:
|
|
100
|
+
* the subgraph resets a cancelled request's `requestedAmount` to 0 (the
|
|
101
|
+
* lender's effective balance is restored) and the original survives only
|
|
102
|
+
* here.
|
|
103
|
+
*/
|
|
104
|
+
initiatedAmount: number | null;
|
|
105
|
+
/**
|
|
106
|
+
* The rejected figure the subgraph reported, parsed with the same `num`
|
|
107
|
+
* rule as the rest: an absent or unparseable value is 0. Unlike
|
|
108
|
+
* `acceptedAmount` this is NOT nullable — it feeds the partial-vs-complete
|
|
109
|
+
* branch, where "no figure" and "nothing rejected" mean the same thing.
|
|
110
|
+
*/
|
|
111
|
+
rejectedAmount: number;
|
|
112
|
+
/**
|
|
113
|
+
* `assetAmount` of the reallocation event, or 0 when the request was not
|
|
114
|
+
* reallocated. The amount that LEFT the requested tranche — which is not
|
|
115
|
+
* necessarily `acceptedAmount`, because a partly-filled request can be
|
|
116
|
+
* reallocated for only part of itself.
|
|
117
|
+
*/
|
|
118
|
+
reallocatedOutAmount: number;
|
|
119
|
+
/**
|
|
120
|
+
* RAW name of the tranche a reallocated deposit was accepted into, or
|
|
121
|
+
* `null` when there was no reallocation. RAW for the same reason
|
|
122
|
+
* `trancheName` is: the app calls `getTrancheDisplayName` on it at the
|
|
123
|
+
* view boundary, and a renamed value must never reach matching code.
|
|
124
|
+
*/
|
|
125
|
+
reallocationTargetTrancheName: string | null;
|
|
126
|
+
/**
|
|
127
|
+
* The row's highest-resolution timestamp: the LATEST event, or
|
|
128
|
+
* `request.timestamp` when the timeline is empty or older. Unix seconds.
|
|
129
|
+
*
|
|
130
|
+
* `firstSubmissionTimestamp` says when the lender asked; this says when
|
|
131
|
+
* anything last happened to the request, which is what a "last updated"
|
|
132
|
+
* column and a recency sort need.
|
|
133
|
+
*/
|
|
134
|
+
lastTimestamp: number;
|
|
135
|
+
/**
|
|
136
|
+
* The signed figure the row shows, chosen by the same branch logic
|
|
137
|
+
* kasu-ui uses: positive for an inflow, negative for an outflow, 0 for a
|
|
138
|
+
* cancelled or fully-rejected request.
|
|
139
|
+
*/
|
|
140
|
+
amount: number;
|
|
141
|
+
/**
|
|
142
|
+
* Submissions bundled into this dNFT-aggregate row (`Initiated` +
|
|
143
|
+
* `Increased`). `> 1` is what drives kasu-ui's inline "(x2)".
|
|
144
|
+
*/
|
|
145
|
+
submissionCount: number;
|
|
146
|
+
/**
|
|
147
|
+
* Timestamp of the FIRST submission in the bundle, or `null` when the
|
|
148
|
+
* bundle carries no submission event yet (the `Initiated` event has not
|
|
149
|
+
* indexed). kasu-ui substitutes `request.timestamp` there; that fallback
|
|
150
|
+
* is the application's to choose, so this layer reports the absence.
|
|
151
|
+
*/
|
|
152
|
+
firstSubmissionTimestamp: number | null;
|
|
153
|
+
/** Has THIS request's cycle closed? — `!canCancel`. See `isCycleClosed`. */
|
|
154
|
+
cycleClosed: boolean;
|
|
155
|
+
/** The SDK's per-pool cancel signal, passed through. */
|
|
156
|
+
canCancel: boolean;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Events that represent a lender SUBMISSION into the bundle. A dNFT position
|
|
161
|
+
* aggregates every submission the lender made into the same pool/tranche this
|
|
162
|
+
* cycle: the first is `Initiated`, each subsequent top-up is `Increased`.
|
|
163
|
+
* Everything else on the timeline (Accepted / Rejected / Cancelled /
|
|
164
|
+
* Reallocated / Forced) is an OUTCOME, not a request, and must not be counted.
|
|
165
|
+
*/
|
|
166
|
+
const SUBMISSION_EVENTS: ReadonlySet<UserRequestEvent['requestType']> = new Set(
|
|
167
|
+
['Initiated', 'Increased'],
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* The submissions bundled into one dNFT-aggregate request row, input order
|
|
172
|
+
* preserved — one loan agreement per submission.
|
|
173
|
+
*/
|
|
174
|
+
export function submissionEvents<
|
|
175
|
+
T extends Pick<UserRequestEvent, 'requestType'>,
|
|
176
|
+
>(events: T[]): T[] {
|
|
177
|
+
return events.filter((e) => SUBMISSION_EVENTS.has(e.requestType));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Count the submissions bundled into one dNFT-aggregate request row. */
|
|
181
|
+
export function countSubmissions(
|
|
182
|
+
events: Pick<UserRequestEvent, 'requestType'>[],
|
|
183
|
+
): number {
|
|
184
|
+
return submissionEvents(events).length;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Timestamp of the FIRST submission in the bundle. Falls back to `fallback`
|
|
189
|
+
* when the `Initiated` event has not indexed yet — the caller decides what
|
|
190
|
+
* that is (kasu-ui passes the request's own timestamp).
|
|
191
|
+
*/
|
|
192
|
+
export function firstSubmissionTimestamp(
|
|
193
|
+
events: Pick<UserRequestEvent, 'requestType' | 'timestamp'>[],
|
|
194
|
+
fallback: number,
|
|
195
|
+
): number {
|
|
196
|
+
const submissions = submissionEvents(events);
|
|
197
|
+
if (submissions.length === 0) return fallback;
|
|
198
|
+
return submissions.reduce(
|
|
199
|
+
(min, e) => (e.timestamp < min ? e.timestamp : min),
|
|
200
|
+
Infinity,
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* The LATEST event timestamp, or `fallback` when the timeline is empty — or
|
|
206
|
+
* when every event predates it, because `fallback` seeds the reduction. That
|
|
207
|
+
* seeding is deliberate and is kasu-ui's behaviour: `request.timestamp` is a
|
|
208
|
+
* fact about the request, and an event indexed with an earlier clock must not
|
|
209
|
+
* make the row look older than the request itself.
|
|
210
|
+
*/
|
|
211
|
+
export function lastEventTimestamp(
|
|
212
|
+
events: Pick<UserRequestEvent, 'timestamp'>[],
|
|
213
|
+
fallback: number,
|
|
214
|
+
): number {
|
|
215
|
+
return events.reduce(
|
|
216
|
+
(max, e) => (e.timestamp > max ? e.timestamp : max),
|
|
217
|
+
fallback,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Has THIS request's cycle closed? — the single open/closed signal behind the
|
|
223
|
+
* status vocabulary and behind Cancel.
|
|
224
|
+
*
|
|
225
|
+
* `request.canCancel` is the SDK's `isCancelable(status, poolId)` —
|
|
226
|
+
* `status !== 'Processed' && !isLendingPoolClearingPending(pool)`. It is
|
|
227
|
+
* per-POOL and reads the same condition the contract enforces on the cancel
|
|
228
|
+
* call — but it reads it ONCE, when the request was fetched. Nothing about
|
|
229
|
+
* this value is live, so a client that holds a request across a cycle close
|
|
230
|
+
* must refetch before acting on it.
|
|
231
|
+
*
|
|
232
|
+
* The raw subgraph `status` must NOT feed this: `'Processing'` is a STICKY
|
|
233
|
+
* historical marker set on the first partial fill and never reset, so gating
|
|
234
|
+
* on it would freeze a partly-filled request in Processing forever. A global
|
|
235
|
+
* settlement clock is equally wrong here — it is blind to whether THIS
|
|
236
|
+
* request's pool is already clearing.
|
|
237
|
+
*/
|
|
238
|
+
export function isCycleClosed(request: Pick<UserRequest, 'canCancel'>): boolean {
|
|
239
|
+
return !request.canCancel;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const isCancelled = (events: UserRequestEvent[]): boolean =>
|
|
243
|
+
events.some((e) => e.requestType === 'Cancelled');
|
|
244
|
+
|
|
245
|
+
const isForced = (events: UserRequestEvent[]): boolean =>
|
|
246
|
+
events.some((e) => e.requestType === 'Forced');
|
|
247
|
+
|
|
248
|
+
const initiatedEvent = (
|
|
249
|
+
events: UserRequestEvent[],
|
|
250
|
+
): UserRequestEvent | undefined =>
|
|
251
|
+
events.find((e) => e.requestType === 'Initiated');
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Subgraph behaviour: when a request is cancelled, the on-request
|
|
255
|
+
* `requestedAmount` field is reset to 0 (the lender's effective balance is
|
|
256
|
+
* restored). The original amount survives on the `Initiated` event's
|
|
257
|
+
* `assetAmount`. Recover from there so cancelled rows still carry the amount
|
|
258
|
+
* the lender originally asked for.
|
|
259
|
+
*/
|
|
260
|
+
const initiatedAmountOf = (events: UserRequestEvent[]): number => {
|
|
261
|
+
const initiated = initiatedEvent(events);
|
|
262
|
+
return initiated ? Number(initiated.assetAmount || '0') : 0;
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* A deposit is REALLOCATED when the timeline carries a `Reallocated` event, or
|
|
267
|
+
* an `Accepted` event into a tranche other than the one requested.
|
|
268
|
+
*/
|
|
269
|
+
const findReallocation = (
|
|
270
|
+
events: UserRequestEvent[],
|
|
271
|
+
originalTrancheId: string,
|
|
272
|
+
): UserRequestEvent | undefined =>
|
|
273
|
+
events.find(
|
|
274
|
+
(e) =>
|
|
275
|
+
e.requestType === 'Reallocated' ||
|
|
276
|
+
(e.requestType === 'Accepted' &&
|
|
277
|
+
e.trancheId.toLowerCase() !==
|
|
278
|
+
originalTrancheId.toLowerCase()),
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
/** kasu-ui's `num`: an absent or unparseable figure reads as 0. */
|
|
282
|
+
const num = (str: string | undefined): number => {
|
|
283
|
+
const n = Number(str ?? '0');
|
|
284
|
+
return Number.isFinite(n) ? n : 0;
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
/** The same parse, keeping "the subgraph reported nothing" distinct from 0. */
|
|
288
|
+
const numOrNull = (str: string | null | undefined): number | null => {
|
|
289
|
+
if (str === undefined || str === null || str.trim() === '') return null;
|
|
290
|
+
const n = Number(str);
|
|
291
|
+
return Number.isFinite(n) ? n : null;
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Convert a `UserRequest` into a `RequestState`. Pure — no clock, no network,
|
|
296
|
+
* no copy.
|
|
297
|
+
*
|
|
298
|
+
* BRANCH ORDER (this IS the behaviour; it reproduces kasu-ui's
|
|
299
|
+
* `deriveTransactionView` check for check):
|
|
300
|
+
*
|
|
301
|
+
* 1. a `Cancelled` event → `cancelled`, neutral, 0
|
|
302
|
+
* 2. a withdrawal with a `Forced` event → `forced`, outflow, −accepted
|
|
303
|
+
* 3. a reallocated deposit → `reallocated`, inflow, +accepted
|
|
304
|
+
* 4. a withdrawal partly filled with a LIVE
|
|
305
|
+
* remainder (cycle still open) → `partial`, outflow, −accepted
|
|
306
|
+
* 5. resolved (`status === 'Processed'`):
|
|
307
|
+
* withdrawal, partly filled → `partial`, outflow, −accepted
|
|
308
|
+
* withdrawal, fully filled → `complete`, outflow, −accepted
|
|
309
|
+
* deposit, nothing accepted → `rejected`, neutral, 0
|
|
310
|
+
* deposit, part rejected → `partial`, inflow, +accepted
|
|
311
|
+
* deposit, fully accepted → `complete`, inflow, +accepted
|
|
312
|
+
* 6. otherwise → `pending`, ±requested
|
|
313
|
+
*
|
|
314
|
+
* `cancelled` is reachable ONLY from branch 1 — a Cancelled EVENT. No
|
|
315
|
+
* processing or resolved state can derive it.
|
|
316
|
+
*
|
|
317
|
+
* Branch 4 is checked BEFORE the resolved branch and gates on `isCycleClosed`
|
|
318
|
+
* (i.e. `canCancel`), never on the sticky raw status: a withdrawal that was
|
|
319
|
+
* partly filled returns to the queue with a live Cancel, and reading the raw
|
|
320
|
+
* status would strand it.
|
|
321
|
+
*
|
|
322
|
+
* WHAT THE APPLICATION STILL OWNS: the status word and the detail line beneath
|
|
323
|
+
* it; the tranche display rename (`getTrancheDisplayName` on `trancheName`
|
|
324
|
+
* and on `reallocationTargetTrancheName`, both of which are RAW here); the
|
|
325
|
+
* pool-name split; the amount format. The "view loan agreement" affordance is
|
|
326
|
+
* a fact, and it follows from two fields already here —
|
|
327
|
+
* `requestType === 'Deposit' && statusCode !== 'cancelled' && statusCode !== 'rejected'`
|
|
328
|
+
* — because neither a cancelled nor a fully-rejected deposit ever issued one,
|
|
329
|
+
* and withdrawals sign no agreement at all.
|
|
330
|
+
*/
|
|
331
|
+
export function deriveRequestState(request: UserRequest): RequestState {
|
|
332
|
+
const isWithdrawal = request.requestType === 'Withdrawal';
|
|
333
|
+
const cancelled = isCancelled(request.events);
|
|
334
|
+
// Cancelled requests have `requestedAmount` zeroed on the request itself;
|
|
335
|
+
// pull the original value from the Initiated event so the row still
|
|
336
|
+
// carries "100 cancelled" instead of "0 cancelled".
|
|
337
|
+
const requested = cancelled
|
|
338
|
+
? num(request.requestedAmount) || initiatedAmountOf(request.events)
|
|
339
|
+
: num(request.requestedAmount);
|
|
340
|
+
const accepted = num(request.acceptedAmount);
|
|
341
|
+
const rejected = num(request.rejectedAmount);
|
|
342
|
+
const forced = isWithdrawal && isForced(request.events);
|
|
343
|
+
const reallocation =
|
|
344
|
+
!isWithdrawal && findReallocation(request.events, request.trancheId);
|
|
345
|
+
|
|
346
|
+
const initiated = initiatedEvent(request.events);
|
|
347
|
+
|
|
348
|
+
const base = {
|
|
349
|
+
id: request.id,
|
|
350
|
+
contractId: initiated?.id ?? '',
|
|
351
|
+
poolId: request.lendingPool.id.toLowerCase(),
|
|
352
|
+
poolName: request.lendingPool.name,
|
|
353
|
+
trancheName: request.trancheName,
|
|
354
|
+
trancheId: request.trancheId,
|
|
355
|
+
fixedTermConfigId: request.fixedTermConfig?.configId ?? '0',
|
|
356
|
+
requestType: request.requestType,
|
|
357
|
+
rawStatus: request.status,
|
|
358
|
+
requestedAmount: requested,
|
|
359
|
+
acceptedAmount: numOrNull(request.acceptedAmount),
|
|
360
|
+
initiatedAmount: initiated
|
|
361
|
+
? initiatedAmountOf(request.events)
|
|
362
|
+
: null,
|
|
363
|
+
rejectedAmount: rejected,
|
|
364
|
+
reallocatedOutAmount: reallocation ? num(reallocation.assetAmount) : 0,
|
|
365
|
+
reallocationTargetTrancheName: reallocation
|
|
366
|
+
? reallocation.trancheName
|
|
367
|
+
: null,
|
|
368
|
+
lastTimestamp: lastEventTimestamp(request.events, request.timestamp),
|
|
369
|
+
submissionCount: countSubmissions(request.events),
|
|
370
|
+
// `firstSubmissionTimestamp` needs a fallback it will never use here:
|
|
371
|
+
// the bundle is non-empty on every path that reaches the call.
|
|
372
|
+
firstSubmissionTimestamp:
|
|
373
|
+
countSubmissions(request.events) === 0
|
|
374
|
+
? null
|
|
375
|
+
: firstSubmissionTimestamp(request.events, 0),
|
|
376
|
+
cycleClosed: isCycleClosed(request),
|
|
377
|
+
canCancel: request.canCancel,
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
// 1. Cancelled wins over everything — the ONLY path to `cancelled`.
|
|
381
|
+
if (cancelled) {
|
|
382
|
+
return { ...base, kind: 'neutral', amount: 0, statusCode: 'cancelled' };
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// 2. Forced withdrawal (credit originator returned funds early).
|
|
386
|
+
if (forced) {
|
|
387
|
+
return {
|
|
388
|
+
...base,
|
|
389
|
+
kind: 'outflow',
|
|
390
|
+
amount: -accepted,
|
|
391
|
+
statusCode: 'forced',
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// 3. Reallocated deposit (accepted into a different lending option).
|
|
396
|
+
if (reallocation) {
|
|
397
|
+
return {
|
|
398
|
+
...base,
|
|
399
|
+
kind: 'inflow',
|
|
400
|
+
amount: accepted,
|
|
401
|
+
statusCode: 'reallocated',
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// 4. Withdrawal partly filled with a LIVE remainder: the request returns
|
|
406
|
+
// to the queue with a live Cancel. Checked BEFORE the resolved branch and
|
|
407
|
+
// via `isCycleClosed`, never via the sticky raw status.
|
|
408
|
+
if (
|
|
409
|
+
isWithdrawal &&
|
|
410
|
+
accepted > 0 &&
|
|
411
|
+
accepted < requested &&
|
|
412
|
+
!isCycleClosed(request)
|
|
413
|
+
) {
|
|
414
|
+
return {
|
|
415
|
+
...base,
|
|
416
|
+
kind: 'outflow',
|
|
417
|
+
amount: -accepted,
|
|
418
|
+
statusCode: 'partial',
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// 5. Resolved — a rejection IS a resolution.
|
|
423
|
+
if (request.status === UserRequestStatus.PROCESSED) {
|
|
424
|
+
if (isWithdrawal) {
|
|
425
|
+
const partly = accepted < requested;
|
|
426
|
+
return {
|
|
427
|
+
...base,
|
|
428
|
+
kind: 'outflow',
|
|
429
|
+
amount: -accepted,
|
|
430
|
+
statusCode: partly ? 'partial' : 'complete',
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Deposit: full reject vs partial vs full accept.
|
|
435
|
+
if (accepted === 0) {
|
|
436
|
+
return {
|
|
437
|
+
...base,
|
|
438
|
+
kind: 'neutral',
|
|
439
|
+
amount: 0,
|
|
440
|
+
statusCode: 'rejected',
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
if (rejected > 0) {
|
|
444
|
+
return {
|
|
445
|
+
...base,
|
|
446
|
+
kind: 'inflow',
|
|
447
|
+
amount: accepted,
|
|
448
|
+
statusCode: 'partial',
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
return {
|
|
452
|
+
...base,
|
|
453
|
+
kind: 'inflow',
|
|
454
|
+
amount: accepted,
|
|
455
|
+
statusCode: 'complete',
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// 6. Unresolved. `cycleClosed` tells the caller whether to render its
|
|
460
|
+
// "queued" or its "processing" word; the code is the same either way.
|
|
461
|
+
return {
|
|
462
|
+
...base,
|
|
463
|
+
kind: isWithdrawal ? 'outflow' : 'inflow',
|
|
464
|
+
amount: isWithdrawal ? -requested : requested,
|
|
465
|
+
statusCode: 'pending',
|
|
466
|
+
};
|
|
467
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cycles and the clearing window — the protocol's weekly clock, as numbers.
|
|
3
|
+
*
|
|
4
|
+
* Lifted verbatim from kasu-ui's `features/portfolio/lib/settlement-window.ts`
|
|
5
|
+
* and `features/lending/lib/cycle-dates.ts`. The two formatters that live
|
|
6
|
+
* beside `deriveCycleDates` there (`formatCycleDate`, `formatCycleCloseUtc`)
|
|
7
|
+
* print words and stay in the applications; everything here is unix seconds in
|
|
8
|
+
* and unix seconds out.
|
|
9
|
+
*
|
|
10
|
+
* The clearing window is the fixed 48 hours immediately preceding an epoch
|
|
11
|
+
* end. Inside it, pending requests are being processed and cannot be modified,
|
|
12
|
+
* and the countdown runs to the epoch end. Outside it, the countdown runs to
|
|
13
|
+
* the next clearing-window start.
|
|
14
|
+
*
|
|
15
|
+
* The epoch end comes straight from the chain (`nextEpochStartTimestamp`, i.e.
|
|
16
|
+
* the SDK's `getNextEpochDate`) — the same value the protocol's own
|
|
17
|
+
* `getNextClearingPeriodDate` derives from — so the window always lines up
|
|
18
|
+
* with the real weekly schedule (Tue 06:00 → Thu 06:00 UTC on Base) instead of
|
|
19
|
+
* a projected subgraph timestamp that can drift off the grid.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Clearing-window length — a fixed 48h protocol constant. Exported so every
|
|
24
|
+
* consumer derives the window from the same number this module runs on.
|
|
25
|
+
*/
|
|
26
|
+
export const CLEARING_WINDOW_SECONDS = 48 * 60 * 60;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Weekly cadence — the epoch schedule is fixed weekly (Tue → Thu UTC on Base).
|
|
30
|
+
* Used only to roll a cycle forward when a request lands inside a window that
|
|
31
|
+
* has already closed.
|
|
32
|
+
*/
|
|
33
|
+
const WEEK_SECONDS = 7 * 24 * 60 * 60;
|
|
34
|
+
|
|
35
|
+
export type SettlementWindowState =
|
|
36
|
+
| {
|
|
37
|
+
phase: 'awaiting';
|
|
38
|
+
secondsUntilClearing: number;
|
|
39
|
+
nextClearingStart: number;
|
|
40
|
+
}
|
|
41
|
+
| { phase: 'clearing'; secondsUntilEpochEnd: number; epochEnd: number }
|
|
42
|
+
| { phase: 'unknown' };
|
|
43
|
+
|
|
44
|
+
export interface SettlementWindowInput {
|
|
45
|
+
/** Now, in unix seconds. */
|
|
46
|
+
nowSeconds: number;
|
|
47
|
+
/**
|
|
48
|
+
* Authoritative epoch end, in unix seconds — the chain's
|
|
49
|
+
* `nextEpochStartTimestamp` (SDK `getNextEpochDate`). During an epoch this
|
|
50
|
+
* is in the future; it refetches to the next boundary once it rolls.
|
|
51
|
+
*/
|
|
52
|
+
nextEpochStart: number;
|
|
53
|
+
/** Clearing-window length; defaults to the 48h protocol constant. */
|
|
54
|
+
clearingWindowSeconds?: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Which phase of the weekly cycle `nowSeconds` falls in, and how long is left
|
|
59
|
+
* of it.
|
|
60
|
+
*
|
|
61
|
+
* `'unknown'` when no epoch boundary has been loaded yet, or when the one on
|
|
62
|
+
* hand is stale (it has already elapsed — the on-chain value refetches to the
|
|
63
|
+
* next boundary shortly after rollover). A caller must render its "no cycle
|
|
64
|
+
* loaded" state there, never a zeroed countdown.
|
|
65
|
+
*/
|
|
66
|
+
export function computeSettlementWindow({
|
|
67
|
+
nowSeconds,
|
|
68
|
+
nextEpochStart,
|
|
69
|
+
clearingWindowSeconds = CLEARING_WINDOW_SECONDS,
|
|
70
|
+
}: SettlementWindowInput): SettlementWindowState {
|
|
71
|
+
// No epoch boundary loaded yet, or a stale one that already elapsed.
|
|
72
|
+
if (nextEpochStart <= 0 || nextEpochStart <= nowSeconds) {
|
|
73
|
+
return { phase: 'unknown' };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const clearingStart = nextEpochStart - clearingWindowSeconds;
|
|
77
|
+
|
|
78
|
+
if (nowSeconds < clearingStart) {
|
|
79
|
+
return {
|
|
80
|
+
phase: 'awaiting',
|
|
81
|
+
secondsUntilClearing: clearingStart - nowSeconds,
|
|
82
|
+
nextClearingStart: clearingStart,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// nowSeconds is in [clearingStart, nextEpochStart) — inside the window.
|
|
87
|
+
return {
|
|
88
|
+
phase: 'clearing',
|
|
89
|
+
secondsUntilEpochEnd: nextEpochStart - nowSeconds,
|
|
90
|
+
epochEnd: nextEpochStart,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The next cycle boundary strictly after `nowSeconds`, in unix seconds — the
|
|
96
|
+
* cycle close (`nextEpochStart − 48h`) while the window is still open, the
|
|
97
|
+
* epoch end once we are inside it.
|
|
98
|
+
*
|
|
99
|
+
* `undefined` when there is no boundary left to wait for: no epoch boundary
|
|
100
|
+
* loaded, or a cached one that has already elapsed — the same staleness rule
|
|
101
|
+
* `computeSettlementWindow` applies before it reports `'unknown'`.
|
|
102
|
+
*
|
|
103
|
+
* Split out of the state machine because some consumers need the INSTANT
|
|
104
|
+
* rather than the phase: one to flush the cycle-dependent caches when the
|
|
105
|
+
* clock crosses it, one to move a pre-commit screen's snapshot clock at the
|
|
106
|
+
* same moment.
|
|
107
|
+
*/
|
|
108
|
+
export function nextCycleBoundary(
|
|
109
|
+
nextEpochStart: number | undefined,
|
|
110
|
+
nowSeconds: number,
|
|
111
|
+
clearingWindowSeconds: number = CLEARING_WINDOW_SECONDS,
|
|
112
|
+
): number | undefined {
|
|
113
|
+
if (!nextEpochStart || nextEpochStart <= 0) return undefined;
|
|
114
|
+
const clearingStart = nextEpochStart - clearingWindowSeconds;
|
|
115
|
+
if (nowSeconds < clearingStart) return clearingStart;
|
|
116
|
+
if (nowSeconds < nextEpochStart) return nextEpochStart;
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface CycleDates {
|
|
121
|
+
/** Unix seconds — the next cycle close (clearing-window start). */
|
|
122
|
+
close: number;
|
|
123
|
+
/** Unix seconds — the outcome-confirmed-by boundary (close + 48h). */
|
|
124
|
+
outcome: number;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The cycle-close and outcome dates for a request submitted `now`.
|
|
129
|
+
*
|
|
130
|
+
* - The cycle "closes" (stops accepting requests, starts processing) at the
|
|
131
|
+
* start of the 48h clearing window, i.e. 48h before the epoch end.
|
|
132
|
+
* - Processing takes up to 48h, so the outcome is confirmed by the epoch end
|
|
133
|
+
* (close + 48h).
|
|
134
|
+
*
|
|
135
|
+
* Returns `null` when the epoch boundary is not available or is stale — a
|
|
136
|
+
* caller then omits the dates entirely (omit, don't stub).
|
|
137
|
+
*
|
|
138
|
+
* The common case is a request submitted OUTSIDE the clearing window: the
|
|
139
|
+
* close is `nextEpochStart − 48h` and the outcome is `nextEpochStart`. When
|
|
140
|
+
* the request lands INSIDE the current clearing window (that close is already
|
|
141
|
+
* in the past), it queues for the NEXT weekly cycle, so the close is advanced
|
|
142
|
+
* by whole weeks until it is in the future.
|
|
143
|
+
*/
|
|
144
|
+
export function deriveCycleDates(
|
|
145
|
+
nextEpochStart: number | undefined,
|
|
146
|
+
nowSeconds: number,
|
|
147
|
+
): CycleDates | null {
|
|
148
|
+
// No boundary loaded, or a stale one that already elapsed — the same
|
|
149
|
+
// staleness rule as `computeSettlementWindow`.
|
|
150
|
+
if (!nextEpochStart || nextEpochStart <= nowSeconds) return null;
|
|
151
|
+
|
|
152
|
+
let close = nextEpochStart - CLEARING_WINDOW_SECONDS;
|
|
153
|
+
let outcome = nextEpochStart;
|
|
154
|
+
// Inside the current clearing window the close already passed; a request
|
|
155
|
+
// now is queued for the next weekly cycle.
|
|
156
|
+
while (close <= nowSeconds) {
|
|
157
|
+
close += WEEK_SECONDS;
|
|
158
|
+
outcome += WEEK_SECONDS;
|
|
159
|
+
}
|
|
160
|
+
return { close, outcome };
|
|
161
|
+
}
|