@kasufinance/kasu-sdk 2.11.0 → 2.13.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.
Files changed (70) hide show
  1. package/README.md +37 -2
  2. package/dist/bundle.cjs.js +1516 -71
  3. package/dist/bundle.esm.js +1511 -73
  4. package/dist/domain/index.d.ts +6 -2
  5. package/dist/domain/index.js +2 -0
  6. package/dist/domain/index.js.map +1 -1
  7. package/dist/domain/pool-tvl.d.ts +62 -0
  8. package/dist/domain/pool-tvl.js +22 -0
  9. package/dist/domain/pool-tvl.js.map +1 -0
  10. package/dist/domain/yield-history.d.ts +122 -0
  11. package/dist/domain/yield-history.js +95 -0
  12. package/dist/domain/yield-history.js.map +1 -0
  13. package/dist/facade/chain-configs.js +24 -0
  14. package/dist/facade/chain-configs.js.map +1 -1
  15. package/dist/facade/kasu.d.ts +2 -0
  16. package/dist/facade/kasu.js +13 -5
  17. package/dist/facade/kasu.js.map +1 -1
  18. package/dist/facade/strategies.d.ts +20 -0
  19. package/dist/facade/strategies.js +22 -0
  20. package/dist/facade/strategies.js.map +1 -1
  21. package/dist/facade/types.d.ts +20 -0
  22. package/dist/facade/user-portfolio.d.ts +32 -0
  23. package/dist/facade/user-portfolio.js +34 -0
  24. package/dist/facade/user-portfolio.js.map +1 -1
  25. package/dist/index.d.ts +5 -0
  26. package/dist/index.js +2 -0
  27. package/dist/index.js.map +1 -1
  28. package/dist/kasu-sdk.d.ts +9 -1
  29. package/dist/kasu-sdk.js +2 -2
  30. package/dist/kasu-sdk.js.map +1 -1
  31. package/dist/sdk-config.d.ts +20 -0
  32. package/dist/sdk-config.js +3 -1
  33. package/dist/sdk-config.js.map +1 -1
  34. package/dist/services/DataService/data-service.d.ts +81 -1
  35. package/dist/services/DataService/data-service.js +211 -6
  36. package/dist/services/DataService/data-service.js.map +1 -1
  37. package/dist/services/DataService/tvl-history.d.ts +296 -0
  38. package/dist/services/DataService/tvl-history.js +558 -0
  39. package/dist/services/DataService/tvl-history.js.map +1 -0
  40. package/dist/services/Portfolio/portfolio.d.ts +37 -0
  41. package/dist/services/Portfolio/portfolio.js +136 -56
  42. package/dist/services/Portfolio/portfolio.js.map +1 -1
  43. package/dist/services/Portfolio/queries.js +15 -3
  44. package/dist/services/Portfolio/queries.js.map +1 -1
  45. package/dist/services/Portfolio/types.d.ts +8 -0
  46. package/dist/services/Portfolio/yield-history.d.ts +162 -0
  47. package/dist/services/Portfolio/yield-history.js +378 -0
  48. package/dist/services/Portfolio/yield-history.js.map +1 -0
  49. package/dist/services/shared.d.ts +16 -0
  50. package/dist/services/shared.js +30 -0
  51. package/dist/services/shared.js.map +1 -1
  52. package/package.json +1 -1
  53. package/src/domain/index.ts +15 -10
  54. package/src/domain/pool-tvl.ts +66 -0
  55. package/src/domain/yield-history.ts +194 -0
  56. package/src/facade/chain-configs.ts +26 -0
  57. package/src/facade/kasu.ts +20 -2
  58. package/src/facade/strategies.ts +26 -0
  59. package/src/facade/types.ts +20 -0
  60. package/src/facade/user-portfolio.ts +40 -3
  61. package/src/index.ts +11 -0
  62. package/src/kasu-sdk.ts +19 -2
  63. package/src/sdk-config.ts +22 -0
  64. package/src/services/DataService/data-service.ts +296 -6
  65. package/src/services/DataService/tvl-history.ts +853 -0
  66. package/src/services/Portfolio/portfolio.ts +263 -153
  67. package/src/services/Portfolio/queries.ts +15 -3
  68. package/src/services/Portfolio/types.ts +10 -0
  69. package/src/services/Portfolio/yield-history.ts +541 -0
  70. package/src/services/shared.ts +38 -1
@@ -1,18 +1,21 @@
1
1
  import { readItems, readSingleton } from '@directus/sdk';
2
2
  import { Provider } from '@ethersproject/abstract-provider';
3
- import { Signer } from 'ethers';
4
- import { formatUnits } from 'ethers/lib/utils';
3
+ import { BigNumber, providers, Signer } from 'ethers';
4
+ import { formatUnits, isAddress } from 'ethers/lib/utils';
5
5
  import { GraphQLClient } from 'graphql-request';
6
6
 
7
7
  import {
8
+ ISystemVariablesAbi,
9
+ ISystemVariablesAbi__factory,
8
10
  KasuPoolExternalTVLAbi,
9
11
  KasuPoolExternalTVLAbi__factory,
10
12
  } from '../../contracts';
13
+ import { poolTvl, PoolTvlHistoryEntry } from '../../domain/pool-tvl';
11
14
  import { epochRateToApy } from '../../domain/rates';
12
15
  import { SdkConfig } from '../../sdk-config';
13
16
  import { getSystemVariablesQuery } from '../Locking/queries';
14
17
  import { SystemVariables } from '../Locking/types';
15
- import { filterArray } from '../shared';
18
+ import { filterArray, isEmptyHistoryError } from '../shared';
16
19
 
17
20
  import {
18
21
  createDirectusClient,
@@ -44,6 +47,26 @@ import {
44
47
  TrancheConfigurationSubgraphResult,
45
48
  TrancheSubgraphResult,
46
49
  } from './subgraph-types';
50
+ import {
51
+ buildPoolTvlHistory,
52
+ Cancellation,
53
+ ClearingsScope,
54
+ createOffChainTvlReader,
55
+ distinctBlocks,
56
+ fetchOffChainTvlAtBlocks,
57
+ fetchPoolBalancesAtBlocks,
58
+ fetchPoolClearings,
59
+ GraphRequest,
60
+ OffChainTvlReader,
61
+ PastBlockCache,
62
+ PoolTvlHistoryOptions,
63
+ resolvePoolTvlHistoryWindow,
64
+ runCancellable,
65
+ TVL_HISTORY_CACHE_DEPTH,
66
+ TVL_HISTORY_DEFAULT_TIMEOUT_MS,
67
+ TVL_HISTORY_RPC_TIMEOUT_MS,
68
+ validatePoolTvlHistoryOptions,
69
+ } from './tvl-history';
47
70
  import {
48
71
  BadAndDoubtfulDebts,
49
72
  FinancialReportingDocuments,
@@ -58,21 +81,44 @@ import {
58
81
  TrancheData,
59
82
  } from './types';
60
83
 
84
+ /** Thrown by `getPoolTvlHistory` when no provider that serves past state is configured. */
85
+ export const NO_HISTORY_PROVIDER_MESSAGE =
86
+ 'getPoolTvlHistory: no history provider; pass historyProvider or set historyRpcUrls to RPCs that serve past state';
87
+
61
88
  export class DataService {
62
89
  private readonly _graph: GraphQLClient;
63
90
  private readonly _externalTvlAbi: KasuPoolExternalTVLAbi;
91
+ private readonly _systemVariablesAbi: ISystemVariablesAbi;
64
92
  private readonly _directus: KasuDirectusClient;
65
93
  private _directusPoolOverview: PoolOverviewDirectus[] | undefined;
94
+ private readonly _explicitHistoryProvider: Provider | undefined;
95
+ private _builtHistoryProviders: Provider[] | undefined;
96
+ /** Immutable past-block reads, kept for the life of this instance. */
97
+ private readonly _tvlHistoryCache = new PastBlockCache();
66
98
 
99
+ /**
100
+ * @param historyProvider - a provider that SERVES PAST STATE (an archive
101
+ * RPC), used only by `getPoolTvlHistory` for the off-chain TVL at old
102
+ * blocks. Without one, providers are built on `SdkConfig.historyRpcUrls`
103
+ * (tried in order); with neither, that one call throws
104
+ * `NO_HISTORY_PROVIDER_MESSAGE`. Never the default provider: a pruned
105
+ * node refuses old blocks.
106
+ */
67
107
  constructor(
68
108
  private _kasuConfig: SdkConfig,
69
109
  signerOrProvider: Signer | Provider,
110
+ historyProvider?: Provider,
70
111
  ) {
71
112
  this._externalTvlAbi = KasuPoolExternalTVLAbi__factory.connect(
72
113
  _kasuConfig.contracts.ExternalTVL,
73
114
  signerOrProvider,
74
115
  );
116
+ this._systemVariablesAbi = ISystemVariablesAbi__factory.connect(
117
+ _kasuConfig.contracts.SystemVariables,
118
+ signerOrProvider,
119
+ );
75
120
  this._graph = new GraphQLClient(_kasuConfig.subgraphUrl);
121
+ this._explicitHistoryProvider = historyProvider;
76
122
 
77
123
  this._directus = createDirectusClient(_kasuConfig.directusUrl);
78
124
  }
@@ -196,6 +242,249 @@ export class DataService {
196
242
  return tvlMap;
197
243
  }
198
244
 
245
+ /**
246
+ * The pool's TVL — the total lent into it by everyone — at every cycle
247
+ * boundary, then as of now: the data behind a TVL chart.
248
+ *
249
+ * One `clearing` point per FINISHED clearing, read at the block it
250
+ * finished in (`LendingPoolClearing.endBlockNumber`), then one `current`
251
+ * point. Every point is `poolTvl(onChain, offChain)`, the definition
252
+ * `getPoolOverview` uses for `totalValueLocked.total`:
253
+ *
254
+ * - on-chain: `lendingPool.balance` from the subgraph, read with
255
+ * time-travel queries (`block: { number }`);
256
+ * - off-chain: `ExternalTVL.externalTVLOfPool(pool)` at the same block,
257
+ * through the HISTORY providers (see the constructor). 0 before the
258
+ * contract was deployed (decided by `getCode`, and only if the same
259
+ * provider shows the pool's own code there); any failed read throws.
260
+ *
261
+ * The `current` point mixes two sources: the subgraph's latest balance
262
+ * (and its latest block time) and `externalTVLOfPool` at `latest` on the
263
+ * history provider. `getPoolOverview` reads the same two, the off-chain
264
+ * one on the default provider, so the last point equals
265
+ * `totalValueLocked.total` when both reads succeed. They can differ when
266
+ * `getPoolOverview`'s off-chain read FAILS — it then falls back to 0 —
267
+ * or when the figure moves between the two calls.
268
+ *
269
+ * Units: `tvl` in display units of the deployment's own stable token
270
+ * (USDC on Base and XDC USDC, AUDD on XDC AUDD); `timestamp` in unix
271
+ * seconds. Ascending in time. See `PoolTvlHistoryEntry`.
272
+ *
273
+ * Cost: one subgraph request for the clearings (per thousand), one per
274
+ * hundred distinct uncached clearing blocks, a `currentEpochNumber` read;
275
+ * then, on the history providers, one `externalTVLOfPool` per distinct
276
+ * uncached block plus the latest, and one to about log2(n) `getCode`s —
277
+ * at most `TVL_HISTORY_RPC_CONCURRENCY` in flight. Final blocks are
278
+ * cached on this instance, so a second call reads only what is new.
279
+ * `timeoutMs` (default 30 s) bounds the whole call.
280
+ *
281
+ * `[]` for a pool the subgraph does not know, for a window with no epoch
282
+ * in it, and on a LITE deployment whose subgraph lacks the entities. On a
283
+ * full deployment that schema error throws, like every network, indexing
284
+ * or past-state failure.
285
+ *
286
+ * @throws RangeError / TypeError on a malformed option or a pool id that
287
+ * is not an address, and `NO_HISTORY_PROVIDER_MESSAGE` when the
288
+ * deployment has an `ExternalTVL` contract but no history provider — all
289
+ * before any network read. `PoolTvlHistoryTimeoutError` /
290
+ * `PoolTvlHistoryAbortedError` on the deadline or the signal,
291
+ * `PastStateUnavailableError` from a pruned history provider.
292
+ */
293
+ async getPoolTvlHistory(
294
+ poolId: string,
295
+ options: PoolTvlHistoryOptions = {},
296
+ ): Promise<PoolTvlHistoryEntry[]> {
297
+ validatePoolTvlHistoryOptions(options);
298
+ if (!isAddress(poolId)) {
299
+ throw new TypeError(
300
+ `getPoolTvlHistory: poolId must be an address, got ${String(poolId)}`,
301
+ );
302
+ }
303
+ const pool = poolId.toLowerCase();
304
+ const externalTvl = this._externalTvlAddress();
305
+ const rpcs = externalTvl ? this._historyRpcs() : [];
306
+ if (externalTvl && rpcs.length === 0) {
307
+ throw new Error(NO_HISTORY_PROVIDER_MESSAGE);
308
+ }
309
+
310
+ return await runCancellable(
311
+ options.timeoutMs ?? TVL_HISTORY_DEFAULT_TIMEOUT_MS,
312
+ options.signal,
313
+ (cancellation) =>
314
+ this._readPoolTvlHistory(
315
+ pool,
316
+ options,
317
+ externalTvl
318
+ ? this._createOffChainTvlReader({
319
+ externalTvl,
320
+ pool,
321
+ rpcs,
322
+ cancellation,
323
+ })
324
+ : null,
325
+ externalTvl ?? '',
326
+ cancellation,
327
+ ),
328
+ );
329
+ }
330
+
331
+ private async _readPoolTvlHistory(
332
+ pool: string,
333
+ options: PoolTvlHistoryOptions,
334
+ reader: OffChainTvlReader | null,
335
+ externalTvl: string,
336
+ cancellation: Cancellation,
337
+ ): Promise<PoolTvlHistoryEntry[]> {
338
+ const graph: GraphRequest = <T>(
339
+ document: string,
340
+ variables: Record<string, unknown>,
341
+ signal?: AbortSignal,
342
+ ): Promise<T> =>
343
+ this._graph.request<T>({ document, variables, signal });
344
+ const scope: ClearingsScope =
345
+ options.fromEpoch === undefined && options.count !== undefined
346
+ ? { kind: 'newest', count: options.count }
347
+ : { kind: 'from', fromEpoch: options.fromEpoch ?? 0 };
348
+
349
+ try {
350
+ const [currentEpochNumber, { clearings, snapshot }] =
351
+ await Promise.all([
352
+ this._systemVariablesAbi.currentEpochNumber(),
353
+ fetchPoolClearings(graph, pool, scope, cancellation),
354
+ ]);
355
+ const currentEpoch = currentEpochNumber.toNumber();
356
+
357
+ const window = resolvePoolTvlHistoryWindow(options, currentEpoch);
358
+ const poolNow = snapshot.pool;
359
+ if (!window || !poolNow) return [];
360
+ const head = snapshot.meta?.block;
361
+ if (!head || typeof head.timestamp !== 'number') {
362
+ throw new Error(
363
+ 'getPoolTvlHistory: the subgraph returned no latest block time',
364
+ );
365
+ }
366
+
367
+ const inWindow = clearings.filter(
368
+ (c) =>
369
+ c.epochId >= window.fromEpoch &&
370
+ c.epochId <= window.toEpoch,
371
+ );
372
+ const blocks = distinctBlocks(inWindow);
373
+ const withCurrent = window.toEpoch >= currentEpoch;
374
+ const cacheable = (block: number): boolean =>
375
+ block <= head.number - TVL_HISTORY_CACHE_DEPTH;
376
+
377
+ const [balances, offChainRead] = await Promise.all([
378
+ this._balancesAtBlocks(graph, pool, blocks, cacheable, cancellation),
379
+ reader
380
+ ? fetchOffChainTvlAtBlocks(reader, blocks, {
381
+ withLatest: withCurrent,
382
+ cancellation,
383
+ cache: this._tvlHistoryCache,
384
+ cacheKey: (kind, block) =>
385
+ `${kind}|${externalTvl}|${pool}|${String(block)}`,
386
+ cacheable,
387
+ })
388
+ : {
389
+ offChain: new Map(
390
+ blocks.map((b) => [b, BigNumber.from(0)]),
391
+ ),
392
+ latest: null,
393
+ },
394
+ ]);
395
+
396
+ return buildPoolTvlHistory({
397
+ clearings: inWindow,
398
+ balances,
399
+ offChain: offChainRead.offChain,
400
+ current: withCurrent
401
+ ? {
402
+ epochId: currentEpoch,
403
+ timestamp: head.timestamp,
404
+ balance: poolNow.balance,
405
+ offChain: offChainRead.latest ?? BigNumber.from(0),
406
+ }
407
+ : null,
408
+ decimals: this._kasuConfig.stableAssetDecimals,
409
+ });
410
+ } catch (error) {
411
+ if (isEmptyHistoryError(error, this._kasuConfig.isLiteDeployment)) {
412
+ return [];
413
+ }
414
+ throw error;
415
+ }
416
+ }
417
+
418
+ /** The pool's balance at each block: cached final blocks, then one batch. */
419
+ private async _balancesAtBlocks(
420
+ graph: GraphRequest,
421
+ pool: string,
422
+ blocks: readonly number[],
423
+ cacheable: (block: number) => boolean,
424
+ cancellation: Cancellation,
425
+ ): Promise<Map<number, string>> {
426
+ const key = (block: number): string =>
427
+ `balance|${this._kasuConfig.subgraphUrl}|${pool}|${String(block)}`;
428
+ const balances = new Map<number, string>();
429
+ const missing: number[] = [];
430
+ for (const block of blocks) {
431
+ const hit = cacheable(block)
432
+ ? this._tvlHistoryCache.get<string>(key(block))
433
+ : undefined;
434
+ if (hit === undefined) missing.push(block);
435
+ else balances.set(block, hit);
436
+ }
437
+ if (missing.length > 0) {
438
+ const read = await fetchPoolBalancesAtBlocks(
439
+ graph,
440
+ pool,
441
+ missing,
442
+ cancellation,
443
+ );
444
+ for (const [block, balance] of read.balances) {
445
+ balances.set(block, balance);
446
+ if (cacheable(block)) this._tvlHistoryCache.set(key(block), balance);
447
+ }
448
+ }
449
+ return balances;
450
+ }
451
+
452
+ /** `null` when the deployment has no `ExternalTVL` contract. */
453
+ private _externalTvlAddress(): string | null {
454
+ const address = this._kasuConfig.contracts.ExternalTVL;
455
+ if (!address || /^0x0{40}$/i.test(address)) return null;
456
+ return address.toLowerCase();
457
+ }
458
+
459
+ /** Seam for tests: the real reader over the history RPCs. */
460
+ private _createOffChainTvlReader(
461
+ args: Parameters<typeof createOffChainTvlReader>[0],
462
+ ): OffChainTvlReader {
463
+ return createOffChainTvlReader(args);
464
+ }
465
+
466
+ /**
467
+ * The history RPCs, in fallback order: the explicit provider, else ONE
468
+ * provider per `historyRpcUrls` entry — built here and nowhere else,
469
+ * pinned to `SdkConfig.chainId` when known, each with a per-call timeout.
470
+ * Built once per instance.
471
+ */
472
+ private _historyRpcs(): Provider[] {
473
+ if (this._explicitHistoryProvider) return [this._explicitHistoryProvider];
474
+ if (!this._builtHistoryProviders) {
475
+ // Through the `ethers` namespace, so the consumer's own build
476
+ // (Node or browser) is used — see `defaultProvider` in the facade.
477
+ this._builtHistoryProviders = this._kasuConfig.historyRpcUrls.map(
478
+ (url) =>
479
+ new providers.StaticJsonRpcProvider(
480
+ { url, timeout: TVL_HISTORY_RPC_TIMEOUT_MS },
481
+ this._kasuConfig.chainId,
482
+ ),
483
+ );
484
+ }
485
+ return this._builtHistoryProviders;
486
+ }
487
+
199
488
  async getPoolOverview(
200
489
  epochId: string,
201
490
  id_in?: string[],
@@ -436,9 +725,10 @@ export class DataService {
436
725
  subheading: lendingPoolDirectus?.subheading,
437
726
  operatingSince: lendingPoolDirectus?.operatingSince,
438
727
  totalValueLocked: {
439
- total: (
440
- parseFloat(lendingPoolSubgraph.balance) +
441
- parseFloat(offChainTvl)
728
+ // The one TVL definition, shared with getPoolTvlHistory.
729
+ total: poolTvl(
730
+ lendingPoolSubgraph.balance,
731
+ offChainTvl,
442
732
  ).toString(),
443
733
  offchain: offChainTvl,
444
734
  },