@kasufinance/kasu-sdk 2.13.0 → 2.14.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 +40 -1
- package/dist/bundle.cjs.js +580 -60
- package/dist/bundle.esm.js +577 -61
- package/dist/domain/index.d.ts +2 -2
- package/dist/domain/pool-tvl.d.ts +104 -0
- package/dist/domain/requests.d.ts +11 -10
- package/dist/facade/index.d.ts +2 -0
- package/dist/facade/strategies.d.ts +17 -1
- package/dist/facade/strategy-deployments.d.ts +65 -0
- package/dist/index.d.ts +1 -1
- package/dist/services/DataService/data-service.d.ts +29 -1
- package/dist/services/DataService/tvl-history.d.ts +49 -0
- package/dist/services/UserLending/subgraph-types.d.ts +2 -8
- package/dist/services/UserLending/types.d.ts +10 -0
- package/dist/services/UserLending/user-lending.d.ts +14 -0
- package/package.json +1 -1
- package/src/domain/index.ts +7 -2
- package/src/domain/pool-tvl.ts +281 -0
- package/src/domain/requests.ts +37 -23
- package/src/facade/index.ts +10 -0
- package/src/facade/strategies.ts +26 -1
- package/src/facade/strategy-deployments.ts +161 -0
- package/src/index.ts +4 -1
- package/src/services/DataService/data-service.ts +103 -7
- package/src/services/DataService/tvl-history.ts +131 -0
- package/src/services/UserLending/queries.ts +2 -7
- package/src/services/UserLending/subgraph-types.ts +2 -8
- package/src/services/UserLending/types.ts +10 -0
- package/src/services/UserLending/user-lending.ts +98 -40
package/src/domain/pool-tvl.ts
CHANGED
|
@@ -64,3 +64,284 @@ export interface PoolTvlHistoryEntry {
|
|
|
64
64
|
/** `'clearing'` for a cycle boundary; `'current'` for the closing point. */
|
|
65
65
|
kind: 'clearing' | 'current';
|
|
66
66
|
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* One deployment's share of a strategy's history, for `combineTvlHistories`:
|
|
70
|
+
* that pool's `getPoolTvlHistoryWindow` result (`{ entries, opening }`) as it
|
|
71
|
+
* came back, and what one unit of the deployment's stable token is worth in
|
|
72
|
+
* USD (`1` for USDC; today's AUD→USD for AUDD). The SDK fetches no exchange
|
|
73
|
+
* rate: the caller passes it.
|
|
74
|
+
*/
|
|
75
|
+
export interface TvlHistoryLeg {
|
|
76
|
+
/** At least one point: a real pool always returns its `current` point. */
|
|
77
|
+
entries: readonly PoolTvlHistoryEntry[];
|
|
78
|
+
/** USD per unit of the leg's stable token. Finite and above 0. */
|
|
79
|
+
usdRate: number;
|
|
80
|
+
/**
|
|
81
|
+
* The leg's step BEFORE its first entry: the newest clearing before the
|
|
82
|
+
* window, as `getPoolTvlHistoryWindow` read it at its own block. It
|
|
83
|
+
* counts from its own `timestamp` on, and the leg is 0 before that — no
|
|
84
|
+
* value is carried back to a time before it existed. A `'clearing'`
|
|
85
|
+
* point no later than the first entry; it is never drawn. `null` or
|
|
86
|
+
* absent: the pool has no clearing before the window (it started lending
|
|
87
|
+
* later, say) and counts 0 before its first entry.
|
|
88
|
+
*/
|
|
89
|
+
opening?: PoolTvlHistoryEntry | null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* One point of a strategy's TVL summed across its deployments.
|
|
94
|
+
*
|
|
95
|
+
* There is no `epochId`: each chain clears at its own blocks and times, so a
|
|
96
|
+
* merged point belongs to no single epoch. Plot by `timestamp`.
|
|
97
|
+
*/
|
|
98
|
+
export interface StrategyTvlHistoryEntry {
|
|
99
|
+
/** Unix SECONDS. Strictly ascending across the series. */
|
|
100
|
+
timestamp: number;
|
|
101
|
+
/** The sum over legs of each leg's TVL then, times its `usdRate`. */
|
|
102
|
+
tvlUsd: number;
|
|
103
|
+
/** `'clearing'` for a cycle boundary on any leg; `'current'` for the closing point. */
|
|
104
|
+
kind: 'clearing' | 'current';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* What `combineTvlHistories` refuses, as a code:
|
|
109
|
+
*
|
|
110
|
+
* - `legs` — not a non-empty array (or one with holes);
|
|
111
|
+
* - `leg` — a leg that is not an object;
|
|
112
|
+
* - `usd-rate` — a rate that is not a finite number above 0;
|
|
113
|
+
* - `opening` — an opening that is not a well-formed `'clearing'` point at
|
|
114
|
+
* or before the leg's first entry (or `null`, or absent);
|
|
115
|
+
* - `entries` — a leg's `entries` is not an array;
|
|
116
|
+
* - `empty-leg` — a leg with no entries: pass only the legs you have;
|
|
117
|
+
* - `entry` — an entry that is not an object (or a hole);
|
|
118
|
+
* - `timestamp` / `tvl` — not a finite number at or above 0;
|
|
119
|
+
* - `kind` — neither `'clearing'` nor `'current'`;
|
|
120
|
+
* - `order` — timestamps going backwards within a leg;
|
|
121
|
+
* - `current` — a `current` point that is not the leg's last, or some legs
|
|
122
|
+
* ending in one and others not;
|
|
123
|
+
* - `overflow` — a sum too large to be a finite number.
|
|
124
|
+
*/
|
|
125
|
+
export type TvlHistoryInputErrorCode =
|
|
126
|
+
| 'legs'
|
|
127
|
+
| 'leg'
|
|
128
|
+
| 'usd-rate'
|
|
129
|
+
| 'opening'
|
|
130
|
+
| 'entries'
|
|
131
|
+
| 'empty-leg'
|
|
132
|
+
| 'entry'
|
|
133
|
+
| 'timestamp'
|
|
134
|
+
| 'tvl'
|
|
135
|
+
| 'kind'
|
|
136
|
+
| 'order'
|
|
137
|
+
| 'current'
|
|
138
|
+
| 'overflow';
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* What `combineTvlHistories` throws instead of returning a number it cannot
|
|
142
|
+
* stand behind. `leg` and `entry` are the zero-based position of the
|
|
143
|
+
* offending input, when there is one.
|
|
144
|
+
*/
|
|
145
|
+
export class TvlHistoryInputError extends Error {
|
|
146
|
+
readonly code: TvlHistoryInputErrorCode;
|
|
147
|
+
readonly leg: number | undefined;
|
|
148
|
+
readonly entry: number | undefined;
|
|
149
|
+
|
|
150
|
+
constructor(code: TvlHistoryInputErrorCode, leg?: number, entry?: number) {
|
|
151
|
+
const at = [
|
|
152
|
+
leg === undefined ? '' : ` at leg ${String(leg)}`,
|
|
153
|
+
entry === undefined ? '' : `, entry ${String(entry)}`,
|
|
154
|
+
].join('');
|
|
155
|
+
super(`combineTvlHistories: input refused (${code})${at}`);
|
|
156
|
+
this.name = 'TvlHistoryInputError';
|
|
157
|
+
this.code = code;
|
|
158
|
+
this.leg = leg;
|
|
159
|
+
this.entry = entry;
|
|
160
|
+
Object.setPrototypeOf(this, TvlHistoryInputError.prototype);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function isNonNegativeFinite(value: unknown): value is number {
|
|
165
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= 0;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Throws unless the leg is well formed. Index loops, not `forEach`: a hole in
|
|
170
|
+
* a sparse array is skipped by the array methods and would surface later as
|
|
171
|
+
* a raw `TypeError` instead of a refusal.
|
|
172
|
+
*/
|
|
173
|
+
function validateLeg(leg: unknown, legIndex: number): TvlHistoryLeg {
|
|
174
|
+
if (typeof leg !== 'object' || leg === null) {
|
|
175
|
+
throw new TvlHistoryInputError('leg', legIndex);
|
|
176
|
+
}
|
|
177
|
+
const { entries, usdRate, opening } = leg as Partial<TvlHistoryLeg>;
|
|
178
|
+
if (
|
|
179
|
+
typeof usdRate !== 'number' ||
|
|
180
|
+
!Number.isFinite(usdRate) ||
|
|
181
|
+
usdRate <= 0
|
|
182
|
+
) {
|
|
183
|
+
throw new TvlHistoryInputError('usd-rate', legIndex);
|
|
184
|
+
}
|
|
185
|
+
if (!Array.isArray(entries as unknown)) {
|
|
186
|
+
throw new TvlHistoryInputError('entries', legIndex);
|
|
187
|
+
}
|
|
188
|
+
const list = entries as readonly unknown[];
|
|
189
|
+
if (list.length === 0) {
|
|
190
|
+
throw new TvlHistoryInputError('empty-leg', legIndex);
|
|
191
|
+
}
|
|
192
|
+
let previous = -Infinity;
|
|
193
|
+
for (let index = 0; index < list.length; index += 1) {
|
|
194
|
+
const entry = list[index];
|
|
195
|
+
if (typeof entry !== 'object' || entry === null) {
|
|
196
|
+
throw new TvlHistoryInputError('entry', legIndex, index);
|
|
197
|
+
}
|
|
198
|
+
const { timestamp, tvl, kind } = entry as Partial<PoolTvlHistoryEntry>;
|
|
199
|
+
if (!isNonNegativeFinite(timestamp)) {
|
|
200
|
+
throw new TvlHistoryInputError('timestamp', legIndex, index);
|
|
201
|
+
}
|
|
202
|
+
if (!isNonNegativeFinite(tvl)) {
|
|
203
|
+
throw new TvlHistoryInputError('tvl', legIndex, index);
|
|
204
|
+
}
|
|
205
|
+
if (kind !== 'clearing' && kind !== 'current') {
|
|
206
|
+
throw new TvlHistoryInputError('kind', legIndex, index);
|
|
207
|
+
}
|
|
208
|
+
if (kind === 'current' && index !== list.length - 1) {
|
|
209
|
+
throw new TvlHistoryInputError('current', legIndex, index);
|
|
210
|
+
}
|
|
211
|
+
if (timestamp < previous) {
|
|
212
|
+
throw new TvlHistoryInputError('order', legIndex, index);
|
|
213
|
+
}
|
|
214
|
+
previous = timestamp;
|
|
215
|
+
}
|
|
216
|
+
if (opening !== undefined && opening !== null) {
|
|
217
|
+
const first = list[0] as PoolTvlHistoryEntry;
|
|
218
|
+
const { timestamp, tvl, kind } = (
|
|
219
|
+
typeof opening === 'object' ? opening : {}
|
|
220
|
+
) as Partial<PoolTvlHistoryEntry>;
|
|
221
|
+
if (
|
|
222
|
+
!isNonNegativeFinite(timestamp) ||
|
|
223
|
+
!isNonNegativeFinite(tvl) ||
|
|
224
|
+
kind !== 'clearing' ||
|
|
225
|
+
timestamp > first.timestamp
|
|
226
|
+
) {
|
|
227
|
+
throw new TvlHistoryInputError('opening', legIndex);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return leg as TvlHistoryLeg;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* A strategy's TVL in USD over time, from its deployments' histories — Tax
|
|
235
|
+
* Pay runs on Base (USDC) and on XDC (AUDD), and its total lent is both.
|
|
236
|
+
* Pure: the caller fetches each leg (`strategyDeployments` names them) and
|
|
237
|
+
* passes the exchange rates.
|
|
238
|
+
*
|
|
239
|
+
* A step merge by time. At every distinct timestamp where any leg has a
|
|
240
|
+
* clearing, the value is the sum over legs of that leg's latest point at or
|
|
241
|
+
* before then, times its `usdRate`. A leg's `opening` — what it held just
|
|
242
|
+
* before the window — is one more step: it counts from its own timestamp
|
|
243
|
+
* until the leg's first entry, and the leg is 0 before it (or throughout,
|
|
244
|
+
* before its first entry, without one).
|
|
245
|
+
* Points that share a timestamp — clearings run in one block, or two chains
|
|
246
|
+
* clearing in the same second — are one output point. The series ends with
|
|
247
|
+
* one `current` point at the latest of the legs' `current` timestamps, equal
|
|
248
|
+
* to the sum of their `current` values. Every leg must end in a `current`
|
|
249
|
+
* point, or none may.
|
|
250
|
+
*
|
|
251
|
+
* The window is the union of the legs' windows: the series starts at the
|
|
252
|
+
* earliest first entry of any leg. An `opening` is a value, never a point.
|
|
253
|
+
* Without it, two windows cut at offset clearing times would count the
|
|
254
|
+
* later-starting leg 0 at the left edge and draw a false step up; with it,
|
|
255
|
+
* only a genuine late start (`opening: null`) counts 0.
|
|
256
|
+
*
|
|
257
|
+
* Every leg and entry is validated first: a bad input — an empty leg
|
|
258
|
+
* included — throws `TvlHistoryInputError` (its `code` says why), so the
|
|
259
|
+
* result never holds `NaN` or `Infinity`. The inputs are not modified.
|
|
260
|
+
*
|
|
261
|
+
* ```ts
|
|
262
|
+
* const window = { count: 52 };
|
|
263
|
+
* const points = combineTvlHistories([
|
|
264
|
+
* { ...(await base.strategies.getPoolTvlHistoryWindow(basePool, window)), usdRate: 1 },
|
|
265
|
+
* { ...(await xdc.strategies.getPoolTvlHistoryWindow(xdcPool, window)), usdRate: audToUsd },
|
|
266
|
+
* ]);
|
|
267
|
+
* ```
|
|
268
|
+
*/
|
|
269
|
+
export function combineTvlHistories(
|
|
270
|
+
legs: readonly TvlHistoryLeg[],
|
|
271
|
+
): StrategyTvlHistoryEntry[] {
|
|
272
|
+
if (!Array.isArray(legs as unknown) || legs.length === 0) {
|
|
273
|
+
throw new TvlHistoryInputError('legs');
|
|
274
|
+
}
|
|
275
|
+
const valid: TvlHistoryLeg[] = [];
|
|
276
|
+
for (let index = 0; index < legs.length; index += 1) {
|
|
277
|
+
valid.push(validateLeg(legs[index], index));
|
|
278
|
+
}
|
|
279
|
+
const endsInCurrent = valid.map(
|
|
280
|
+
(leg) => leg.entries[leg.entries.length - 1].kind === 'current',
|
|
281
|
+
);
|
|
282
|
+
const odd = endsInCurrent.findIndex((ends) => ends !== endsInCurrent[0]);
|
|
283
|
+
if (odd !== -1) throw new TvlHistoryInputError('current', odd);
|
|
284
|
+
|
|
285
|
+
const finalTimestamp = endsInCurrent[0]
|
|
286
|
+
? Math.max(
|
|
287
|
+
...valid.map(
|
|
288
|
+
(leg) => leg.entries[leg.entries.length - 1].timestamp,
|
|
289
|
+
),
|
|
290
|
+
)
|
|
291
|
+
: undefined;
|
|
292
|
+
const clearingTimes = [
|
|
293
|
+
...new Set(
|
|
294
|
+
valid.flatMap((leg) =>
|
|
295
|
+
leg.entries
|
|
296
|
+
.filter((entry) => entry.kind === 'clearing')
|
|
297
|
+
.map((entry) => entry.timestamp),
|
|
298
|
+
),
|
|
299
|
+
),
|
|
300
|
+
]
|
|
301
|
+
.filter((t) => finalTimestamp === undefined || t < finalTimestamp)
|
|
302
|
+
.sort((a, b) => a - b);
|
|
303
|
+
|
|
304
|
+
// One cursor per leg: the times ascend, so each leg is walked once. A
|
|
305
|
+
// leg's `current` point is a step like any other, which keeps a chain
|
|
306
|
+
// whose subgraph lags behind the other's last clearing at its own latest.
|
|
307
|
+
const cursors = valid.map(() => -1);
|
|
308
|
+
const sumAt = (t: number): number => {
|
|
309
|
+
let sum = 0;
|
|
310
|
+
valid.forEach((leg, index) => {
|
|
311
|
+
let cursor = cursors[index];
|
|
312
|
+
while (
|
|
313
|
+
cursor + 1 < leg.entries.length &&
|
|
314
|
+
leg.entries[cursor + 1].timestamp <= t
|
|
315
|
+
) {
|
|
316
|
+
cursor += 1;
|
|
317
|
+
}
|
|
318
|
+
cursors[index] = cursor;
|
|
319
|
+
// Before the first entry, the opening is the step — from its
|
|
320
|
+
// own time on, never earlier.
|
|
321
|
+
const opening = leg.opening;
|
|
322
|
+
const value =
|
|
323
|
+
cursor >= 0
|
|
324
|
+
? leg.entries[cursor].tvl
|
|
325
|
+
: opening && opening.timestamp <= t
|
|
326
|
+
? opening.tvl
|
|
327
|
+
: 0;
|
|
328
|
+
sum += value * leg.usdRate;
|
|
329
|
+
});
|
|
330
|
+
if (!Number.isFinite(sum)) throw new TvlHistoryInputError('overflow');
|
|
331
|
+
return sum;
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
const combined: StrategyTvlHistoryEntry[] = clearingTimes.map((t) => ({
|
|
335
|
+
timestamp: t,
|
|
336
|
+
tvlUsd: sumAt(t),
|
|
337
|
+
kind: 'clearing' as const,
|
|
338
|
+
}));
|
|
339
|
+
if (finalTimestamp !== undefined) {
|
|
340
|
+
combined.push({
|
|
341
|
+
timestamp: finalTimestamp,
|
|
342
|
+
tvlUsd: sumAt(finalTimestamp),
|
|
343
|
+
kind: 'current',
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
return combined;
|
|
347
|
+
}
|
package/src/domain/requests.ts
CHANGED
|
@@ -82,10 +82,13 @@ export interface RequestState {
|
|
|
82
82
|
statusCode: RequestStatusCode;
|
|
83
83
|
kind: RequestKind;
|
|
84
84
|
/**
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
* `
|
|
85
|
+
* `UserRequest.requestedAmount`, in assets: the amount still OUTSTANDING,
|
|
86
|
+
* which the subgraph reduces as parts are accepted or rejected (0 once a
|
|
87
|
+
* request is fully resolved). A cancelled request has it zeroed by the
|
|
88
|
+
* subgraph, so for a cancellation it is the `Cancelled` event's amount —
|
|
89
|
+
* what was outstanding when cancelled, top-ups included — falling back to
|
|
90
|
+
* the `Initiated` event (see `initiatedAmount`) only when that event
|
|
91
|
+
* carries none.
|
|
89
92
|
*/
|
|
90
93
|
requestedAmount: number;
|
|
91
94
|
/**
|
|
@@ -287,16 +290,24 @@ const initiatedEvent = (
|
|
|
287
290
|
): UserRequestEvent | undefined =>
|
|
288
291
|
events.find((e) => e.requestType === 'Initiated');
|
|
289
292
|
|
|
293
|
+
/** `assetAmount` of the `Initiated` event, or 0 when there is none. */
|
|
294
|
+
const initiatedAmountOf = (events: UserRequestEvent[]): number => {
|
|
295
|
+
const initiated = initiatedEvent(events);
|
|
296
|
+
return initiated ? Number(initiated.assetAmount || '0') : 0;
|
|
297
|
+
};
|
|
298
|
+
|
|
290
299
|
/**
|
|
291
300
|
* Subgraph behaviour: when a request is cancelled, the on-request
|
|
292
301
|
* `requestedAmount` field is reset to 0 (the lender's effective balance is
|
|
293
|
-
* restored). The
|
|
294
|
-
* `assetAmount
|
|
295
|
-
*
|
|
302
|
+
* restored). The amount cancelled survives on the `Cancelled` event's
|
|
303
|
+
* `assetAmount` — everything still outstanding at the cancel, top-ups
|
|
304
|
+
* included. Only when that event carries no figure does this fall back to the
|
|
305
|
+
* `Initiated` event, which holds the FIRST submission alone and so misses
|
|
306
|
+
* every top-up.
|
|
296
307
|
*/
|
|
297
|
-
const
|
|
298
|
-
const
|
|
299
|
-
return
|
|
308
|
+
const cancelledAmountOf = (events: UserRequestEvent[]): number => {
|
|
309
|
+
const cancelledEvent = events.find((e) => e.requestType === 'Cancelled');
|
|
310
|
+
return num(cancelledEvent?.assetAmount) || initiatedAmountOf(events);
|
|
300
311
|
};
|
|
301
312
|
|
|
302
313
|
/**
|
|
@@ -332,17 +343,15 @@ const numOrNull = (str: string | null | undefined): number | null => {
|
|
|
332
343
|
* Convert a `UserRequest` into a `RequestState`. Pure — no clock, no network,
|
|
333
344
|
* no copy.
|
|
334
345
|
*
|
|
335
|
-
* BRANCH ORDER (this IS the behaviour
|
|
336
|
-
* `deriveTransactionView` check for check):
|
|
346
|
+
* BRANCH ORDER (this IS the behaviour):
|
|
337
347
|
*
|
|
338
348
|
* 1. a `Cancelled` event → `cancelled`, neutral, 0
|
|
339
349
|
* 2. a withdrawal with a `Forced` event → `forced`, outflow, −accepted
|
|
340
350
|
* 3. a reallocated deposit → `reallocated`, inflow, +accepted
|
|
341
|
-
* 4. a withdrawal partly filled
|
|
342
|
-
* remainder (cycle
|
|
351
|
+
* 4. a withdrawal partly filled (paid > 0) with
|
|
352
|
+
* a LIVE remainder (queued > 0, cycle open) → `partial`, outflow, −accepted
|
|
343
353
|
* 5. resolved (`status === 'Processed'`):
|
|
344
|
-
* withdrawal
|
|
345
|
-
* withdrawal, fully filled → `complete`, outflow, −accepted
|
|
354
|
+
* withdrawal (nothing left queued) → `complete`, outflow, −accepted
|
|
346
355
|
* deposit, nothing accepted → `rejected`, neutral, 0
|
|
347
356
|
* deposit, part rejected → `partial`, inflow, +accepted
|
|
348
357
|
* deposit, fully accepted → `complete`, inflow, +accepted
|
|
@@ -369,10 +378,10 @@ export function deriveRequestState(request: UserRequest): RequestState {
|
|
|
369
378
|
const isWithdrawal = request.requestType === 'Withdrawal';
|
|
370
379
|
const cancelled = isCancelled(request.events);
|
|
371
380
|
// Cancelled requests have `requestedAmount` zeroed on the request itself;
|
|
372
|
-
//
|
|
373
|
-
//
|
|
381
|
+
// take the amount from the Cancelled event so the row still carries
|
|
382
|
+
// "1500 cancelled" (1000 + a 500 top-up) instead of "0 cancelled".
|
|
374
383
|
const requested = cancelled
|
|
375
|
-
? num(request.requestedAmount) ||
|
|
384
|
+
? num(request.requestedAmount) || cancelledAmountOf(request.events)
|
|
376
385
|
: num(request.requestedAmount);
|
|
377
386
|
const accepted = num(request.acceptedAmount);
|
|
378
387
|
const rejected = num(request.rejectedAmount);
|
|
@@ -441,11 +450,14 @@ export function deriveRequestState(request: UserRequest): RequestState {
|
|
|
441
450
|
|
|
442
451
|
// 4. Withdrawal partly filled with a LIVE remainder: the request returns
|
|
443
452
|
// to the queue with a live Cancel. Checked BEFORE the resolved branch and
|
|
444
|
-
// via `isCycleClosed`, never via the sticky raw status.
|
|
453
|
+
// via `isCycleClosed`, never via the sticky raw status. `requested` is
|
|
454
|
+
// what is still OUTSTANDING, so "partly filled" is "something paid AND
|
|
455
|
+
// something still queued" — never a comparison of the two, which would
|
|
456
|
+
// turn on whether more was paid than is left.
|
|
445
457
|
if (
|
|
446
458
|
isWithdrawal &&
|
|
447
459
|
accepted > 0 &&
|
|
448
|
-
|
|
460
|
+
requested > 0 &&
|
|
449
461
|
!isCycleClosed(request)
|
|
450
462
|
) {
|
|
451
463
|
return {
|
|
@@ -458,13 +470,15 @@ export function deriveRequestState(request: UserRequest): RequestState {
|
|
|
458
470
|
|
|
459
471
|
// 5. Resolved — a rejection IS a resolution.
|
|
460
472
|
if (request.status === UserRequestStatus.PROCESSED) {
|
|
473
|
+
// A withdrawal is resolved only once nothing is left queued — a
|
|
474
|
+
// partly filled one returns to the queue (branch 4), and one cancelled
|
|
475
|
+
// with a remainder is branch 1 — so a resolved withdrawal is complete.
|
|
461
476
|
if (isWithdrawal) {
|
|
462
|
-
const partly = accepted < requested;
|
|
463
477
|
return {
|
|
464
478
|
...base,
|
|
465
479
|
kind: 'outflow',
|
|
466
480
|
amount: -accepted,
|
|
467
|
-
statusCode:
|
|
481
|
+
statusCode: 'complete',
|
|
468
482
|
};
|
|
469
483
|
}
|
|
470
484
|
|
package/src/facade/index.ts
CHANGED
|
@@ -19,6 +19,16 @@ export { READ_ONLY_MESSAGE } from './read-only';
|
|
|
19
19
|
// Chain configurations
|
|
20
20
|
export { CHAIN_CONFIGS } from './chain-configs';
|
|
21
21
|
|
|
22
|
+
// Strategy ↔ deployment resolution (pure, over CHAIN_CONFIGS)
|
|
23
|
+
export {
|
|
24
|
+
StrategyDeploymentInputError,
|
|
25
|
+
strategyDeployments,
|
|
26
|
+
} from './strategy-deployments';
|
|
27
|
+
export type {
|
|
28
|
+
StrategyDeployment,
|
|
29
|
+
StrategyDeploymentInputErrorCode,
|
|
30
|
+
} from './strategy-deployments';
|
|
31
|
+
|
|
22
32
|
// Directus-backed helpers (I/O — not part of `domain/`)
|
|
23
33
|
export { fetchUnusedPoolIds } from './unused-pool-ids';
|
|
24
34
|
|
package/src/facade/strategies.ts
CHANGED
|
@@ -2,7 +2,10 @@ import { PoolTvlHistoryEntry } from '../domain/pool-tvl';
|
|
|
2
2
|
import { selectVisiblePools } from '../domain/pools';
|
|
3
3
|
import { pickShortDescription } from '../domain/strategy-summary';
|
|
4
4
|
import { DataService } from '../services/DataService/data-service';
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
PoolTvlHistoryOptions,
|
|
7
|
+
PoolTvlHistoryResult,
|
|
8
|
+
} from '../services/DataService/tvl-history';
|
|
6
9
|
import {
|
|
7
10
|
PoolDelegateProfileAndHistory,
|
|
8
11
|
PoolOverview,
|
|
@@ -175,6 +178,28 @@ export class StrategiesFacade {
|
|
|
175
178
|
return await this._dataService.getPoolTvlHistory(poolId, options);
|
|
176
179
|
}
|
|
177
180
|
|
|
181
|
+
/**
|
|
182
|
+
* `getPoolTvlHistory` plus `opening` — the newest clearing before the
|
|
183
|
+
* window, read at its own block like every other point, or `null` when
|
|
184
|
+
* the pool has none — from the same read. Pass the result to
|
|
185
|
+
* `combineTvlHistories` as a leg with its `usdRate`: the opening is what
|
|
186
|
+
* the leg held before its first point, so a window cut at a different
|
|
187
|
+
* time on another chain does not count it 0. Same options and errors as
|
|
188
|
+
* `getPoolTvlHistory`; the extra cost is one clearing row (or, for
|
|
189
|
+
* `fromEpoch`, one small request) and one block.
|
|
190
|
+
*
|
|
191
|
+
* ```ts
|
|
192
|
+
* const leg = await kasu.strategies.getPoolTvlHistoryWindow(poolId, { count: 52 });
|
|
193
|
+
* // { entries: PoolTvlHistoryEntry[], opening: PoolTvlHistoryEntry | null }
|
|
194
|
+
* ```
|
|
195
|
+
*/
|
|
196
|
+
async getPoolTvlHistoryWindow(
|
|
197
|
+
poolId: string,
|
|
198
|
+
options: PoolTvlHistoryOptions = {},
|
|
199
|
+
): Promise<PoolTvlHistoryResult> {
|
|
200
|
+
return await this._dataService.getPoolTvlHistoryWindow(poolId, options);
|
|
201
|
+
}
|
|
202
|
+
|
|
178
203
|
/**
|
|
179
204
|
* Fetch a single strategy by pool ID. Returns `null` if not found.
|
|
180
205
|
*/
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { CHAIN_CONFIGS } from './chain-configs';
|
|
2
|
+
import { ChainConfigEntry, SupportedChain } from './types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Strategy ↔ deployment resolution. A lending strategy is not tied to one
|
|
6
|
+
* deployment: Tax Pay runs as one pool on Base (USDC) and another on XDC
|
|
7
|
+
* (AUDD), and the two share one CMS record keyed by the BASE pool id — the
|
|
8
|
+
* canonical id. Each chain config's `poolMetadataMapping` maps its
|
|
9
|
+
* chain-local ids to canonical ones; Base, being canonical, has none.
|
|
10
|
+
*
|
|
11
|
+
* Pure: it reads only the built-in `CHAIN_CONFIGS`. The same rules as
|
|
12
|
+
* kasu-ui's counterpart lookup, so both apps sum a strategy over the same
|
|
13
|
+
* pools.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** The pool that represents a strategy on one deployment. */
|
|
17
|
+
export interface StrategyDeployment {
|
|
18
|
+
chain: SupportedChain;
|
|
19
|
+
/**
|
|
20
|
+
* Chain-local pool id, lowercase — what that chain's subgraph is keyed
|
|
21
|
+
* by.
|
|
22
|
+
*/
|
|
23
|
+
poolId: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* What `strategyDeployments` refuses, as a code:
|
|
28
|
+
*
|
|
29
|
+
* - `pool-id` — not a `0x` address;
|
|
30
|
+
* - `chain` — not a `SupportedChain`;
|
|
31
|
+
* - `wrong-chain` — the mapping places the id on ANOTHER deployment (an XDC
|
|
32
|
+
* chain-local id passed as Base, or a Base id passed as XDC). Querying one
|
|
33
|
+
* chain with another's address reads nothing, silently.
|
|
34
|
+
*/
|
|
35
|
+
export type StrategyDeploymentInputErrorCode =
|
|
36
|
+
| 'pool-id'
|
|
37
|
+
| 'chain'
|
|
38
|
+
| 'wrong-chain';
|
|
39
|
+
|
|
40
|
+
/** What `strategyDeployments` throws for an id it cannot place. */
|
|
41
|
+
export class StrategyDeploymentInputError extends Error {
|
|
42
|
+
readonly code: StrategyDeploymentInputErrorCode;
|
|
43
|
+
|
|
44
|
+
constructor(code: StrategyDeploymentInputErrorCode) {
|
|
45
|
+
super(`strategyDeployments: input refused (${code})`);
|
|
46
|
+
this.name = 'StrategyDeploymentInputError';
|
|
47
|
+
this.code = code;
|
|
48
|
+
Object.setPrototypeOf(this, StrategyDeploymentInputError.prototype);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The canonical deployment: its pool ids ARE the strategies' ids. */
|
|
53
|
+
const CANONICAL_CHAIN: SupportedChain = 'base';
|
|
54
|
+
|
|
55
|
+
const ADDRESS_RE = /^0x[0-9a-f]{40}$/;
|
|
56
|
+
|
|
57
|
+
type MappingConfigs = Readonly<
|
|
58
|
+
Record<SupportedChain, Pick<ChainConfigEntry, 'poolMetadataMapping'>>
|
|
59
|
+
>;
|
|
60
|
+
|
|
61
|
+
/** The deployment's mapping with both sides lowercased; Base never has one. */
|
|
62
|
+
function mappingOf(
|
|
63
|
+
configs: MappingConfigs,
|
|
64
|
+
chain: SupportedChain,
|
|
65
|
+
): [string, string][] {
|
|
66
|
+
if (chain === CANONICAL_CHAIN) return [];
|
|
67
|
+
return Object.entries(configs[chain].poolMetadataMapping ?? {}).map(
|
|
68
|
+
([local, canonical]) => [local.toLowerCase(), canonical.toLowerCase()],
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* `strategyDeployments` over any configs — the export reads `CHAIN_CONFIGS`;
|
|
74
|
+
* the spec passes its own to cover shapes the built-in mapping lacks.
|
|
75
|
+
*/
|
|
76
|
+
export function resolveStrategyDeployments(
|
|
77
|
+
configs: MappingConfigs,
|
|
78
|
+
poolId: string,
|
|
79
|
+
chain: SupportedChain,
|
|
80
|
+
): StrategyDeployment[] {
|
|
81
|
+
const id = typeof poolId === 'string' ? poolId.trim().toLowerCase() : '';
|
|
82
|
+
if (!ADDRESS_RE.test(id)) {
|
|
83
|
+
throw new StrategyDeploymentInputError('pool-id');
|
|
84
|
+
}
|
|
85
|
+
if (
|
|
86
|
+
typeof chain !== 'string' ||
|
|
87
|
+
!Object.prototype.hasOwnProperty.call(configs, chain)
|
|
88
|
+
) {
|
|
89
|
+
throw new StrategyDeploymentInputError('chain');
|
|
90
|
+
}
|
|
91
|
+
const chains = Object.keys(configs) as SupportedChain[];
|
|
92
|
+
const mappings = chains.map((c) => [c, mappingOf(configs, c)] as const);
|
|
93
|
+
const isLocalElsewhere = mappings.some(
|
|
94
|
+
([c, mapping]) =>
|
|
95
|
+
c !== chain && mapping.some(([local]) => local === id),
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
let canonical: string;
|
|
99
|
+
if (chain === CANONICAL_CHAIN) {
|
|
100
|
+
// A Base id is its own Base pool — unless the mapping says it is
|
|
101
|
+
// another deployment's chain-local id, which names no Base pool.
|
|
102
|
+
if (isLocalElsewhere) {
|
|
103
|
+
throw new StrategyDeploymentInputError('wrong-chain');
|
|
104
|
+
}
|
|
105
|
+
canonical = id;
|
|
106
|
+
} else {
|
|
107
|
+
const own = mappingOf(configs, chain).find(([local]) => local === id);
|
|
108
|
+
if (own) {
|
|
109
|
+
canonical = own[1];
|
|
110
|
+
} else {
|
|
111
|
+
const isCanonical = mappings.some(([, mapping]) =>
|
|
112
|
+
mapping.some(([, target]) => target === id),
|
|
113
|
+
);
|
|
114
|
+
if (isLocalElsewhere || isCanonical) {
|
|
115
|
+
throw new StrategyDeploymentInputError('wrong-chain');
|
|
116
|
+
}
|
|
117
|
+
// Unmapped off Base: a strategy of one on its own chain (every
|
|
118
|
+
// XDC USDC pool today). Claiming a Base counterpart would read
|
|
119
|
+
// Base with an XDC address.
|
|
120
|
+
return [{ chain, poolId: id }];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const found: StrategyDeployment[] = [
|
|
125
|
+
{ chain: CANONICAL_CHAIN, poolId: canonical },
|
|
126
|
+
];
|
|
127
|
+
for (const [c, mapping] of mappings) {
|
|
128
|
+
for (const [local, target] of mapping) {
|
|
129
|
+
if (target === canonical) found.push({ chain: c, poolId: local });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return found;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Every deployment a strategy has a pool on, canonical (Base) first, then the
|
|
137
|
+
* others in `CHAIN_CONFIGS` order — the legs to fetch and pass to
|
|
138
|
+
* `combineTvlHistories`. One entry for a strategy that lives on one chain
|
|
139
|
+
* only (PayFi, on Base).
|
|
140
|
+
*
|
|
141
|
+
* `chain` is the deployment `poolId` was read on — required, because an
|
|
142
|
+
* address alone does not say which chain it belongs to. From either side of
|
|
143
|
+
* a mapping the answer is the same set: Tax Pay's Base id on `'base'` and
|
|
144
|
+
* its XDC id on `'xdc'` both return both pools. An id the mapping does not
|
|
145
|
+
* know is a strategy of one on its own chain. Ids are compared
|
|
146
|
+
* case-insensitively and returned lowercase.
|
|
147
|
+
*
|
|
148
|
+
* Throws `StrategyDeploymentInputError` for a malformed id, an unknown chain,
|
|
149
|
+
* or an id the mapping places on another deployment (`'wrong-chain'`).
|
|
150
|
+
*
|
|
151
|
+
* ```ts
|
|
152
|
+
* strategyDeployments(strategy.id, 'base');
|
|
153
|
+
* // [{ chain: 'base', poolId: '0x…' }, { chain: 'xdc', poolId: '0x…' }]
|
|
154
|
+
* ```
|
|
155
|
+
*/
|
|
156
|
+
export function strategyDeployments(
|
|
157
|
+
poolId: string,
|
|
158
|
+
chain: SupportedChain,
|
|
159
|
+
): StrategyDeployment[] {
|
|
160
|
+
return resolveStrategyDeployments(CHAIN_CONFIGS, poolId, chain);
|
|
161
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -45,7 +45,10 @@ export type {
|
|
|
45
45
|
|
|
46
46
|
export type { YieldHistoryOptions } from './services/Portfolio/yield-history';
|
|
47
47
|
|
|
48
|
-
export type {
|
|
48
|
+
export type {
|
|
49
|
+
PoolTvlHistoryOptions,
|
|
50
|
+
PoolTvlHistoryResult,
|
|
51
|
+
} from './services/DataService/tvl-history';
|
|
49
52
|
export {
|
|
50
53
|
PastStateUnavailableError,
|
|
51
54
|
PoolTvlHistoryAbortedError,
|