@scallop-io/sui-scallop-sdk 1.4.6 → 1.4.7

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.
@@ -26,9 +26,12 @@ import type {
26
26
  ObligationBorrowIcentiveReward,
27
27
  SupportBorrowIncentiveRewardCoins,
28
28
  SupportAssetCoins,
29
+ MarketPools,
30
+ MarketCollaterals,
29
31
  } from '../types';
30
32
  import { SuiObjectRef } from '@mysten/sui/client';
31
33
  import { queryMultipleObjects } from './objectsQuery';
34
+ import { normalizeStructTag, SUI_TYPE_ARG } from '@scallop-io/sui-kit';
32
35
 
33
36
  /**
34
37
  * Get user lending infomation for specific pools.
@@ -43,6 +46,7 @@ export const getLendings = async (
43
46
  query: ScallopQuery,
44
47
  poolCoinNames: SupportPoolCoins[] = [...SUPPORT_POOLS],
45
48
  ownerAddress?: string,
49
+ marketPools?: MarketPools,
46
50
  indexer: boolean = false
47
51
  ) => {
48
52
  const marketCoinNames = poolCoinNames.map((poolCoinName) =>
@@ -53,12 +57,14 @@ export const getLendings = async (
53
57
  ) as SupportStakeMarketCoins[];
54
58
 
55
59
  const coinPrices = await query.utils.getCoinPrices();
56
- const marketPools = (
57
- await query.getMarketPools(poolCoinNames, {
58
- indexer,
59
- coinPrices,
60
- })
61
- ).pools;
60
+ marketPools =
61
+ marketPools ??
62
+ (
63
+ await query.getMarketPools(poolCoinNames, {
64
+ indexer,
65
+ coinPrices,
66
+ })
67
+ ).pools;
62
68
 
63
69
  const spools = await query.getSpools(stakeMarketCoinNames, {
64
70
  indexer,
@@ -311,9 +317,13 @@ export const getLending = async (
311
317
  export const getObligationAccounts = async (
312
318
  query: ScallopQuery,
313
319
  ownerAddress?: string,
320
+ market?: {
321
+ pools: MarketPools;
322
+ collaterals: MarketCollaterals;
323
+ },
314
324
  indexer: boolean = false
315
325
  ) => {
316
- const market = await query.getMarketPools(undefined, { indexer });
326
+ market = market ?? (await query.getMarketPools(undefined, { indexer }));
317
327
  const coinPrices = await query.getAllCoinPrices({
318
328
  marketPools: market.pools,
319
329
  });
@@ -379,7 +389,6 @@ export const getObligationAccount = async (
379
389
  query.queryObligation(obligation),
380
390
  query.getBorrowIncentivePools(undefined, {
381
391
  coinPrices,
382
- indexer,
383
392
  marketPools: market.pools,
384
393
  }),
385
394
  query.getBorrowIncentiveAccounts(obligation),
@@ -838,3 +847,195 @@ export const getTotalValueLocked = async (
838
847
 
839
848
  return tvl;
840
849
  };
850
+
851
+ /**
852
+ * Get user portfolio by wallet address
853
+ */
854
+ export const getUserPortfolio = async (
855
+ query: ScallopQuery,
856
+ walletAddress: string,
857
+ indexer: boolean = false
858
+ ) => {
859
+ const market = await query.getMarketPools();
860
+ const [lendings, obligationAccounts, borrowIncentivePools] =
861
+ await Promise.all([
862
+ query.getLendings(undefined, walletAddress, {
863
+ indexer,
864
+ marketPools: market.pools,
865
+ }),
866
+ query.getObligationAccounts(walletAddress, {
867
+ indexer,
868
+ market: market,
869
+ }),
870
+ query.getBorrowIncentivePools(undefined, {
871
+ marketPools: market.pools,
872
+ }),
873
+ ]);
874
+
875
+ // get pending rewards (spool and borrow incentive)
876
+ const parsedLendings = Object.values(lendings)
877
+ .filter((t) => t.availableWithdrawCoin > 0)
878
+ .map((lending) => ({
879
+ suppliedCoin: lending.availableWithdrawCoin,
880
+ suppliedValue: lending.suppliedValue,
881
+ stakedCoin: lending.availableUnstakeCoin,
882
+ coinName: lending.coinName,
883
+ symbol: lending.symbol,
884
+ coinType: lending.coinType,
885
+ coinPrice: lending.coinPrice,
886
+ coinDecimals: lending.coinDecimal,
887
+ supplyApr: lending.supplyApr,
888
+ supplyApy: lending.supplyApy,
889
+ incentiveApr: isFinite(lending.rewardApr) ? lending.rewardApr : 0,
890
+ }));
891
+
892
+ const parsedObligationAccounts = Object.values(obligationAccounts)
893
+ .filter(
894
+ (t): t is NonNullable<typeof t> =>
895
+ !!t && t.totalBorrowedValueWithWeight > 0
896
+ )
897
+ .map((obligationAccount) => {
898
+ return {
899
+ obligationId: obligationAccount.obligationId,
900
+ totalDebtsInUsd: obligationAccount.totalBorrowedValueWithWeight,
901
+ totalCollateralInUsd: obligationAccount.totalDepositedValue,
902
+ riskLevel: obligationAccount.totalRiskLevel,
903
+ availableCollateralInUsd:
904
+ obligationAccount.totalAvailableCollateralValue,
905
+ totalUnhealthyCollateralInUsd:
906
+ obligationAccount.totalUnhealthyCollateralValue,
907
+ borrowedPools: Object.values(obligationAccount.debts)
908
+ .filter((debt) => debt.borrowedCoin > 0)
909
+ .map((debt) => ({
910
+ coinName: debt.coinName,
911
+ symbol: debt.symbol,
912
+ coinDecimals: debt.coinDecimal,
913
+ coinType: debt.coinType,
914
+ coinPrice: debt.coinPrice,
915
+ borrowedCoin: debt.borrowedCoin,
916
+ borrowedValueInUsd: debt.borrowedValueWithWeight,
917
+ borrowApr: market.pools[debt.coinName]?.borrowApr,
918
+ borrowApy: market.pools[debt.coinName]?.borrowApy,
919
+ incentiveInfos: Object.values(
920
+ borrowIncentivePools[debt.coinName]?.points ?? {}
921
+ )
922
+ .filter((t) => isFinite(t.rewardApr))
923
+ .map((t) => ({
924
+ coinName: t.coinName,
925
+ symbol: t.symbol,
926
+ coinType: t.coinType,
927
+ incentiveApr: t.rewardApr,
928
+ })),
929
+ })),
930
+ };
931
+ });
932
+
933
+ const pendingLendingRewards = Object.values(lendings).reduce(
934
+ (acc, reward) => {
935
+ if (reward.availableClaimCoin === 0) return acc;
936
+ if (!acc[reward.symbol]) {
937
+ acc[reward.symbol] = {
938
+ symbol: reward.symbol,
939
+ coinType: normalizeStructTag(SUI_TYPE_ARG), // @TODO: for now lending reward is all in SUI
940
+ coinPrice: reward.coinPrice,
941
+ pendingRewardInCoin: reward.availableClaimCoin,
942
+ };
943
+ } else {
944
+ acc[reward.symbol].pendingRewardInCoin += reward.availableClaimCoin;
945
+ }
946
+ return acc;
947
+ },
948
+ {} as Record<
949
+ string,
950
+ {
951
+ coinType: string;
952
+ symbol: string;
953
+ coinPrice: number;
954
+ pendingRewardInCoin: number;
955
+ }
956
+ >
957
+ );
958
+
959
+ const pendingBorrowIncentiveRewards = Object.values(obligationAccounts)
960
+ .filter((t): t is NonNullable<typeof t> => !!t)
961
+ .reduce(
962
+ (acc, curr) => {
963
+ Object.values(curr.borrowIncentives).forEach((incentive) => {
964
+ incentive.rewards.forEach((reward) => {
965
+ if (reward.availableClaimCoin === 0) return acc;
966
+ if (!acc[reward.coinName]) {
967
+ acc[reward.coinName] = {
968
+ symbol: reward.symbol,
969
+ coinType: reward.coinType,
970
+ coinPrice: reward.coinPrice,
971
+ pendingRewardInCoin: reward.availableClaimCoin,
972
+ };
973
+ } else {
974
+ acc[reward.coinName].pendingRewardInCoin +=
975
+ reward.availableClaimCoin;
976
+ }
977
+ });
978
+ });
979
+ return acc;
980
+ },
981
+ {} as Record<
982
+ string,
983
+ {
984
+ coinType: string;
985
+ symbol: string;
986
+ coinPrice: number;
987
+ pendingRewardInCoin: number;
988
+ }
989
+ >
990
+ );
991
+ return {
992
+ lendings: {
993
+ totalSupplyValue: parsedLendings.reduce((acc, curr) => {
994
+ acc += curr.suppliedValue;
995
+ return acc;
996
+ }, 0),
997
+ suppliedPools: parsedLendings,
998
+ },
999
+ borrowings: {
1000
+ ...parsedObligationAccounts.reduce(
1001
+ (acc, curr) => {
1002
+ acc.totalDebtValue += curr.totalDebtsInUsd;
1003
+ acc.totalCollateralValue += curr.totalCollateralInUsd;
1004
+ return acc;
1005
+ },
1006
+ {
1007
+ totalDebtValue: 0,
1008
+ totalCollateralValue: 0,
1009
+ } as {
1010
+ totalDebtValue: number;
1011
+ totalCollateralValue: number;
1012
+ }
1013
+ ),
1014
+ obligations: parsedObligationAccounts,
1015
+ },
1016
+ pendingRewards: {
1017
+ lendings: Object.entries(pendingLendingRewards).reduce(
1018
+ (acc, [key, value]) => {
1019
+ acc.push({
1020
+ ...value,
1021
+ coinName: key,
1022
+ pendingRewardInUsd: value.coinPrice * value.pendingRewardInCoin,
1023
+ });
1024
+ return acc;
1025
+ },
1026
+ [] as any
1027
+ ),
1028
+ borrowIncentives: Object.entries(pendingBorrowIncentiveRewards).reduce(
1029
+ (acc, [key, value]) => {
1030
+ acc.push({
1031
+ coinName: key,
1032
+ ...value,
1033
+ pendingRewardInUsd: value.coinPrice * value.pendingRewardInCoin,
1034
+ });
1035
+ return acc;
1036
+ },
1037
+ [] as any
1038
+ ),
1039
+ },
1040
+ };
1041
+ };
@@ -95,11 +95,11 @@ export const getSCoinAmount = async (
95
95
  ) => {
96
96
  const owner = ownerAddress || utils.suiKit.currentAddress();
97
97
  const sCoinType = utils.parseSCoinType(sCoinName);
98
- const amount = await utils.cache.queryGetCoinBalance({
98
+ const coinBalance = await utils.cache.queryGetCoinBalance({
99
99
  owner,
100
100
  coinType: sCoinType,
101
101
  });
102
- return BigNumber(amount).toNumber();
102
+ return BigNumber(coinBalance?.totalBalance ?? '0').toNumber();
103
103
  };
104
104
 
105
105
  const isSupportStakeCoins = (value: string): value is SupportSCoin => {
@@ -10,6 +10,7 @@ import type {
10
10
  } from '../models';
11
11
  import { ScallopCache } from 'src/models/scallopCache';
12
12
  import { AddressesInterface } from './address';
13
+ import { QueryClient, QueryClientConfig } from '@tanstack/query-core';
13
14
 
14
15
  export type ScallopClientFnReturnType<T extends boolean> = T extends true
15
16
  ? SuiTransactionBlockResponse
@@ -26,7 +27,9 @@ export type ScallopBaseInstanceParams = {
26
27
  suiKit?: SuiKit;
27
28
  };
28
29
 
29
- export type ScallopCacheInstanceParams = ScallopBaseInstanceParams;
30
+ export type ScallopCacheInstanceParams = ScallopBaseInstanceParams & {
31
+ queryClient?: QueryClient;
32
+ };
30
33
 
31
34
  export type ScallopAddressInstanceParams = ScallopBaseInstanceParams & {
32
35
  cache?: ScallopCache;
@@ -69,7 +72,8 @@ export type ScallopParams = {
69
72
  export type ScallopClientParams = ScallopParams &
70
73
  ScallopBuilderParams &
71
74
  ScallopQueryParams &
72
- ScallopUtilsParams;
75
+ ScallopUtilsParams &
76
+ ScallopCacheParams;
73
77
 
74
78
  export type ScallopBuilderParams = ScallopParams & {
75
79
  pythEndpoints?: string[];
@@ -81,3 +85,10 @@ export type ScallopQueryParams = ScallopParams & ScallopUtilsParams;
81
85
  export type ScallopUtilsParams = ScallopParams & {
82
86
  pythEndpoints?: string[];
83
87
  };
88
+
89
+ export type ScallopCacheParams = Omit<
90
+ ScallopParams,
91
+ 'addressId' | 'forceAddressesInterface'
92
+ > & {
93
+ cacheOptions?: QueryClientConfig;
94
+ };
@@ -1,7 +1,5 @@
1
1
  export * from './builder';
2
2
  export * from './query';
3
3
  export * from './util';
4
- export * from './tokenBucket';
5
4
  export * from './indexer';
6
5
  export * from './core';
7
- export * from './tokenBucket';
@@ -17,7 +17,9 @@ export async function callMethodWithIndexerFallback(
17
17
  try {
18
18
  return await method.apply(context, args);
19
19
  } catch (e: any) {
20
- console.warn(`Indexer requests failed: ${e}. Retrying without indexer..`);
20
+ console.warn(
21
+ `Indexer requests failed: ${e.message}. Retrying without indexer..`
22
+ );
21
23
  return await method.apply(context, [
22
24
  ...args.slice(0, -1),
23
25
  {
@@ -1,2 +0,0 @@
1
- export declare const DEFAULT_TOKENS_PER_INTERVAL = 10;
2
- export declare const DEFAULT_INTERVAL_IN_MS = 250;
@@ -1,11 +0,0 @@
1
- declare class TokenBucket {
2
- private tokensPerInterval;
3
- private interval;
4
- private tokens;
5
- private lastRefill;
6
- constructor(tokensPerInterval: number, intervalInMs: number);
7
- refill(): void;
8
- removeTokens(count: number): boolean;
9
- }
10
- declare const callWithRateLimit: <T>(tokenBucket: TokenBucket, fn: () => Promise<T>, retryDelayInMs?: number, maxRetries?: number, backoffFactor?: number) => Promise<T | null>;
11
- export { TokenBucket, callWithRateLimit };
@@ -1,2 +0,0 @@
1
- export const DEFAULT_TOKENS_PER_INTERVAL = 10;
2
- export const DEFAULT_INTERVAL_IN_MS = 250;
@@ -1,68 +0,0 @@
1
- import { DEFAULT_INTERVAL_IN_MS } from 'src/constants/tokenBucket';
2
-
3
- class TokenBucket {
4
- private tokensPerInterval: number;
5
- private interval: number;
6
- private tokens: number;
7
- private lastRefill: number;
8
-
9
- constructor(tokensPerInterval: number, intervalInMs: number) {
10
- this.tokensPerInterval = tokensPerInterval;
11
- this.interval = intervalInMs;
12
- this.tokens = tokensPerInterval;
13
- this.lastRefill = Date.now();
14
- }
15
-
16
- refill() {
17
- const now = Date.now();
18
- const elapsed = now - this.lastRefill;
19
-
20
- if (elapsed >= this.interval) {
21
- const tokensToAdd =
22
- Math.floor(elapsed / this.interval) * this.tokensPerInterval;
23
- this.tokens = Math.min(this.tokens + tokensToAdd, this.tokensPerInterval);
24
-
25
- // Update lastRefill to reflect the exact time of the last "refill"
26
- this.lastRefill += Math.floor(elapsed / this.interval) * this.interval;
27
- }
28
- }
29
-
30
- removeTokens(count: number) {
31
- this.refill();
32
- if (this.tokens >= count) {
33
- this.tokens -= count;
34
- return true;
35
- }
36
- return false;
37
- }
38
- }
39
-
40
- const callWithRateLimit = async <T>(
41
- tokenBucket: TokenBucket,
42
- fn: () => Promise<T>,
43
- retryDelayInMs = DEFAULT_INTERVAL_IN_MS,
44
- maxRetries = 15,
45
- backoffFactor = 1.25 // The factor by which to increase the delay
46
- ): Promise<T | null> => {
47
- let retries = 0;
48
-
49
- const tryRequest = async (): Promise<T | null> => {
50
- if (tokenBucket.removeTokens(1)) {
51
- const result = await fn();
52
- return result;
53
- } else if (retries < maxRetries) {
54
- retries++;
55
- const delay = retryDelayInMs * Math.pow(backoffFactor, retries);
56
- // console.error(`Rate limit exceeded, retrying in ${delay} ms`);
57
- await new Promise((resolve) => setTimeout(resolve, delay));
58
- return tryRequest();
59
- } else {
60
- console.error('Maximum retries reached');
61
- return null;
62
- }
63
- };
64
-
65
- return tryRequest();
66
- };
67
-
68
- export { TokenBucket, callWithRateLimit };