@evaafi/sdk 0.7.0 → 0.9.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 (122) hide show
  1. package/dist/api/feeds.d.ts +30 -0
  2. package/dist/api/feeds.js +73 -0
  3. package/dist/api/liquidation.js +1 -1
  4. package/dist/api/math.js +1 -1
  5. package/dist/api/parser.d.ts +4 -2
  6. package/dist/api/parser.js +39 -18
  7. package/dist/api/parsers/AbstractOracleParser.d.ts +11 -0
  8. package/dist/api/parsers/AbstractOracleParser.js +9 -0
  9. package/dist/api/parsers/ClassicOracleParser.d.ts +10 -0
  10. package/dist/api/parsers/ClassicOracleParser.js +16 -0
  11. package/dist/api/parsers/PythOracleParser.d.ts +17 -0
  12. package/dist/api/parsers/PythOracleParser.js +22 -0
  13. package/dist/api/parsers/index.d.ts +3 -0
  14. package/dist/api/parsers/index.js +19 -0
  15. package/dist/api/prices.d.ts +6 -6
  16. package/dist/api/prices.js +37 -46
  17. package/dist/api/pyth.d.ts +16 -0
  18. package/dist/api/pyth.js +35 -0
  19. package/dist/constants/assets/assetId.d.ts +22 -0
  20. package/dist/constants/assets/assetId.js +29 -0
  21. package/dist/constants/assets/index.d.ts +3 -0
  22. package/dist/constants/assets/index.js +19 -0
  23. package/dist/constants/assets/mainnet.d.ts +19 -0
  24. package/dist/constants/assets/mainnet.js +114 -0
  25. package/dist/constants/assets/testnet.d.ts +14 -0
  26. package/dist/constants/assets/testnet.js +54 -0
  27. package/dist/constants/general/index.d.ts +65 -0
  28. package/dist/constants/general/index.js +93 -0
  29. package/dist/constants/general/mainnet.d.ts +24 -0
  30. package/dist/constants/general/mainnet.js +53 -0
  31. package/dist/constants/general/testnet.d.ts +12 -0
  32. package/dist/constants/general/testnet.js +15 -0
  33. package/dist/constants/general.d.ts +2 -2
  34. package/dist/constants/general.js +4 -4
  35. package/dist/constants/index.d.ts +3 -0
  36. package/dist/constants/index.js +19 -0
  37. package/dist/constants/pools/index.d.ts +2 -0
  38. package/dist/constants/pools/index.js +18 -0
  39. package/dist/constants/pools/mainnet.d.ts +14 -0
  40. package/dist/constants/pools/mainnet.js +145 -0
  41. package/dist/constants/pools/testnet.d.ts +9 -0
  42. package/dist/constants/pools/testnet.js +57 -0
  43. package/dist/constants/pools.js +7 -7
  44. package/dist/contracts/AbstractMaster.d.ts +185 -0
  45. package/dist/contracts/AbstractMaster.js +179 -0
  46. package/dist/contracts/ClassicMaster.d.ts +34 -0
  47. package/dist/contracts/ClassicMaster.js +87 -0
  48. package/dist/contracts/PythMaster.d.ts +61 -0
  49. package/dist/contracts/PythMaster.js +179 -0
  50. package/dist/contracts/UserContract.d.ts +1 -7
  51. package/dist/contracts/UserContract.js +1 -19
  52. package/dist/contracts/index.d.ts +5 -0
  53. package/dist/contracts/index.js +21 -0
  54. package/dist/index.d.ts +14 -14
  55. package/dist/index.js +20 -60
  56. package/dist/prices/Oracle.interface.d.ts +9 -0
  57. package/dist/prices/Oracle.interface.js +2 -0
  58. package/dist/prices/Prices.d.ts +5 -3
  59. package/dist/prices/Prices.js +13 -3
  60. package/dist/prices/PricesCollector.d.ts +17 -7
  61. package/dist/prices/PricesCollector.js +67 -51
  62. package/dist/prices/PythCollector.d.ts +22 -0
  63. package/dist/prices/PythCollector.js +217 -0
  64. package/dist/prices/Types.d.ts +17 -1
  65. package/dist/prices/Types.js +8 -1
  66. package/dist/prices/index.d.ts +4 -3
  67. package/dist/prices/index.js +4 -3
  68. package/dist/prices/sources/Backend.d.ts +5 -4
  69. package/dist/prices/sources/Backend.js +16 -13
  70. package/dist/prices/sources/Icp.d.ts +2 -1
  71. package/dist/prices/sources/Icp.js +12 -9
  72. package/dist/prices/sources/PriceSource.d.ts +7 -6
  73. package/dist/prices/utils.d.ts +10 -8
  74. package/dist/prices/utils.js +32 -46
  75. package/dist/types/Master.d.ts +10 -30
  76. package/dist/types/Master.js +3 -0
  77. package/dist/utils/userJettonWallet.js +0 -8
  78. package/dist/utils/utils.d.ts +8 -1
  79. package/dist/utils/utils.js +31 -2
  80. package/package.json +3 -2
  81. package/src/api/feeds.ts +90 -0
  82. package/src/api/liquidation.ts +1 -1
  83. package/src/api/math.ts +1 -1
  84. package/src/api/parser.ts +100 -38
  85. package/src/api/parsers/AbstractOracleParser.ts +16 -0
  86. package/src/api/parsers/ClassicOracleParser.ts +20 -0
  87. package/src/api/parsers/PythOracleParser.ts +34 -0
  88. package/src/api/parsers/index.ts +3 -0
  89. package/src/api/prices.ts +32 -41
  90. package/src/constants/assets/assetId.ts +30 -0
  91. package/src/constants/assets/index.ts +3 -0
  92. package/src/constants/{assets.ts → assets/mainnet.ts} +3 -96
  93. package/src/constants/assets/testnet.ts +74 -0
  94. package/src/constants/general/index.ts +91 -0
  95. package/src/constants/{general.ts → general/mainnet.ts} +48 -72
  96. package/src/constants/general/testnet.ts +25 -0
  97. package/src/constants/index.ts +3 -0
  98. package/src/constants/pools/index.ts +2 -0
  99. package/src/constants/pools/mainnet.ts +218 -0
  100. package/src/constants/pools/testnet.ts +75 -0
  101. package/src/contracts/AbstractMaster.ts +450 -0
  102. package/src/contracts/ClassicMaster.ts +149 -0
  103. package/src/contracts/PythMaster.ts +313 -0
  104. package/src/contracts/UserContract.ts +7 -28
  105. package/src/contracts/index.ts +7 -0
  106. package/src/index.ts +18 -85
  107. package/src/prices/Oracle.interface.ts +18 -0
  108. package/src/prices/Prices.ts +17 -4
  109. package/src/prices/PricesCollector.ts +91 -68
  110. package/src/prices/PythCollector.ts +294 -0
  111. package/src/prices/Types.ts +28 -6
  112. package/src/prices/index.ts +4 -3
  113. package/src/prices/sources/Backend.ts +21 -19
  114. package/src/prices/sources/Icp.ts +13 -10
  115. package/src/prices/sources/PriceSource.ts +6 -5
  116. package/src/prices/utils.ts +65 -68
  117. package/src/types/Master.ts +29 -52
  118. package/src/types/User.ts +15 -7
  119. package/src/utils/userJettonWallet.ts +0 -8
  120. package/src/utils/utils.ts +41 -2
  121. package/src/constants/pools.ts +0 -177
  122. package/src/contracts/MasterContract.ts +0 -410
@@ -1,146 +1,169 @@
1
- import { Cell, Dictionary } from "@ton/core";
2
- import { MAINNET_POOL_CONFIG } from "../constants/pools";
3
- import { PoolAssetConfig, PoolAssetsConfig, PoolConfig } from "../types/Master";
4
- import { PriceSource } from "./sources";
5
- import { DefaultPriceSourcesConfig, PriceSourcesConfig, RawPriceData } from "./Types";
6
- import { collectAndFilterPrices, generatePriceSources, getMedianPrice, packAssetsData, packOraclesData, packPrices, verifyPricesSign, verifyPricesTimestamp } from "./utils";
7
- import { delay } from "../utils/utils";
8
- import { Prices } from "./Prices";
9
- import { checkNotInDebtAtAll } from "../api/math";
10
-
11
-
12
- export class PricesCollector {
1
+ import { Cell, Dictionary } from "@ton/core"
2
+ import { checkNotInDebtAtAll } from "../api/math"
3
+ import { ExtendedEvaaOracle, PoolAssetConfig, PoolAssetsConfig } from "../types/Master"
4
+ import { FetchConfig, proxyFetchRetries } from '../utils/utils'
5
+ import { Oracle } from "./Oracle.interface"
6
+ import { Prices } from "./Prices"
7
+ import { PriceSource } from "./sources"
8
+ import { DefaultPriceSourcesConfig, PriceSourcesConfig, RawPriceData } from "./Types"
9
+ import { collectAndFilterPrices, generatePriceSources, getMedianPrice, packAssetsData, packOraclesData, packPrices, verifyPricesTimestamp } from "./utils"
10
+
11
+
12
+ export type PricesCollectorConfig = {
13
+ poolAssetsConfig: PoolAssetsConfig;
14
+ minimalOracles: number;
15
+ evaaOracles: ExtendedEvaaOracle[];
16
+ sourcesConfig?: PriceSourcesConfig;
17
+ additionalPriceSources?: PriceSource[];
18
+ };
19
+
20
+ export class PricesCollector implements Oracle {
13
21
  #prices: RawPriceData[];
14
- #poolConfig: PoolConfig;
22
+ #poolAssetsConfig: PoolAssetsConfig;
15
23
  #sourcesConfig: PriceSourcesConfig;
16
24
  #priceSources: PriceSource[];
17
-
18
- constructor(poolConfig: PoolConfig = MAINNET_POOL_CONFIG, sourcesConfig: PriceSourcesConfig = DefaultPriceSourcesConfig, additionalPriceSources?: PriceSource[]) {
19
- this.#poolConfig = poolConfig;
20
- this.#sourcesConfig = sourcesConfig;
21
- this.#priceSources = generatePriceSources(this.#sourcesConfig, this.#poolConfig.oracles);
22
-
23
- if (additionalPriceSources) {
24
- this.#priceSources.push(...additionalPriceSources);
25
+ #minimalOracles: number;
26
+
27
+ constructor(config: PricesCollectorConfig) {
28
+ this.#poolAssetsConfig = config.poolAssetsConfig;
29
+ this.#sourcesConfig = config.sourcesConfig ?? DefaultPriceSourcesConfig;
30
+ this.#priceSources = generatePriceSources(this.#sourcesConfig, config.evaaOracles);
31
+ this.#minimalOracles = config.minimalOracles;
32
+ if (config.additionalPriceSources) {
33
+ this.#priceSources.push(...config.additionalPriceSources);
25
34
  }
26
-
27
35
  this.#prices = [];
28
36
  }
29
37
 
30
- // TODO Make UserData class and incapsulate raw bigintegers
31
-
32
- async getPricesForLiquidate(realPrincipals: Dictionary<bigint, bigint>, retries: number = 1, timeout: number = 3000): Promise<Prices> {
38
+ async getPricesForLiquidate(realPrincipals: Dictionary<bigint, bigint>, fetchConfig?: FetchConfig): Promise<Prices> {
33
39
  const assets = this.#filterEmptyPrincipalsAndAssets(realPrincipals);
34
40
  if (assets.includes(undefined)) {
35
41
  throw new Error("User from another pool");
36
42
  }
37
- return await this.getPrices(assets.map(x => x!), retries, timeout);
43
+ return await this.getPrices(assets.map(x => x!), fetchConfig);
38
44
  }
39
45
 
40
-
41
- async getPricesForWithdraw(realPrincipals: Dictionary<bigint, bigint>, withdrawAsset: PoolAssetConfig, collateralToDebt = false, retries: number = 1, timeout: number = 3000): Promise<Prices> {
46
+ async getPricesForWithdraw(realPrincipals: Dictionary<bigint, bigint>, withdrawAsset: PoolAssetConfig, collateralToDebt = false, fetchConfig?: FetchConfig): Promise<Prices> {
42
47
  let assets = this.#filterEmptyPrincipalsAndAssets(realPrincipals);
43
48
  if (checkNotInDebtAtAll(realPrincipals) && (realPrincipals.get(withdrawAsset.assetId) ?? 0n) > 0n && !collateralToDebt) {
44
49
  return new Prices(Dictionary.empty<bigint, bigint>(), Cell.EMPTY);
45
50
  }
46
-
47
51
  if (assets.includes(undefined)) {
48
52
  throw new Error("User from another pool");
49
53
  }
50
-
51
54
  if (!assets.includes(withdrawAsset)) {
52
55
  assets.push(withdrawAsset);
53
56
  }
54
-
55
57
  if (collateralToDebt && assets.length == 1) {
56
58
  throw new Error("Cannot debt only one supplied asset");
57
59
  }
58
-
59
- return await this.getPrices(assets.map(x => x!), retries, timeout);
60
+ return await this.getPrices(assets.map(x => x!), fetchConfig);
60
61
  }
61
62
 
62
- async getPrices(assets: PoolAssetsConfig = this.#poolConfig.poolAssetsConfig, retries: number = 1, timeout: number = 3000): Promise<Prices> {
63
- console.debug('[getPrices] Assets length', assets.length);
63
+ async getPricesForSupplyWithdraw(
64
+ realPrincipals: Dictionary<bigint, bigint>,
65
+ supplyAsset: PoolAssetConfig | undefined,
66
+ withdrawAsset: PoolAssetConfig | undefined,
67
+ collateralToDebt: boolean,
68
+ fetchConfig?: FetchConfig,
69
+ ): Promise<Prices> {
70
+ // Используем ту же логику, что и getPricesForWithdraw, но supplyAsset не используется
71
+ let assets = this.#filterEmptyPrincipalsAndAssets(realPrincipals);
72
+ if (checkNotInDebtAtAll(realPrincipals) && withdrawAsset && (realPrincipals.get(withdrawAsset.assetId) ?? 0n) > 0n && !collateralToDebt) {
73
+ return new Prices(Dictionary.empty<bigint, bigint>(), Cell.EMPTY);
74
+ }
75
+ if (assets.includes(undefined)) {
76
+ throw new Error("User from another pool");
77
+ }
78
+ if (withdrawAsset && !assets.includes(withdrawAsset)) {
79
+ assets.push(withdrawAsset);
80
+ }
81
+ if (collateralToDebt && assets.length == 1) {
82
+ throw new Error("Cannot debt only one supplied asset");
83
+ }
84
+ return await this.getPrices(assets.map(x => x!), fetchConfig);
85
+ }
64
86
 
87
+ async getPrices(assets: PoolAssetsConfig = this.#poolAssetsConfig, fetchConfig?: FetchConfig): Promise<Prices> {
65
88
  if (assets.length == 0) {
66
89
  return new Prices(Dictionary.empty<bigint, bigint>(), Cell.EMPTY);
67
90
  }
68
91
 
69
- for (let i = 0; i <= retries; i++) { // attemts = retries + 1
70
- if (!this.#prices || this.#filterPrices() < this.#poolConfig.minimalOracles) {
71
- //console.debug('[getPrices] Load prices attemp', i + 1)
72
- if (i > 0) {
73
- await delay(timeout);
74
- }
75
- await this.#collectPrices();
76
- } else {
77
- break;
78
- }
79
- }
92
+ await this.#collectPricesWithValidation(fetchConfig);
80
93
 
81
- if (this.#prices.length < this.#poolConfig.minimalOracles) {
82
- throw new Error(`Error per updating prices, valid ${this.#prices.length} of ${this.#poolConfig.minimalOracles}`); // if still not enough data after retries
94
+ if (this.#prices.length < this.#minimalOracles) {
95
+ throw new Error(`Error per updating prices, valid ${this.#prices.length} of ${this.#minimalOracles}`);
83
96
  }
84
97
  const prices = this.#getPricesByAssetList(assets);
85
98
  return new Prices(prices.dict, prices.dataCell);
86
99
  }
87
100
 
88
101
  #getPricesByAssetList(assets: PoolAssetsConfig) {
89
- //console.debug('[getPricesByAssetList] start')
90
- let pricesFiltered = this.#prices; // for strict check this.#prices.filter(x => assets.every(asset => x.dict.has(asset.assetId)));
91
-
92
- if (pricesFiltered.length < this.#poolConfig.minimalOracles) {
102
+ let pricesFiltered = this.#prices;
103
+ if (pricesFiltered.length < this.#minimalOracles) {
93
104
  throw new Error("Not enough price data");
94
105
  }
95
-
96
- if (pricesFiltered.length > this.#poolConfig.minimalOracles) {
106
+ if (pricesFiltered.length > this.#minimalOracles) {
97
107
  const sortedByTimestamp = pricesFiltered.slice().sort((a, b) => b.timestamp - a.timestamp);
98
- const newerPrices = sortedByTimestamp.slice(0, this.#poolConfig.minimalOracles);
108
+ const newerPrices = sortedByTimestamp.slice(0, this.#minimalOracles);
99
109
  pricesFiltered = newerPrices.sort((a, b) => a.oracleId - b.oracleId);
100
110
  }
101
-
102
111
  const medianData = assets.map((asset) => ({
103
112
  assetId: asset.assetId,
104
113
  medianPrice: getMedianPrice(pricesFiltered, asset.assetId),
105
114
  }));
106
-
107
115
  const nonEmptymedianData = medianData.filter(x => x.medianPrice != null) as { assetId: bigint, medianPrice: bigint }[];
108
-
109
116
  const packedMedianData = packAssetsData(nonEmptymedianData);
110
-
111
117
  const oraclesData = pricesFiltered.map((x) => ({
112
118
  oracle: { id: x.oracleId, pubkey: x.pubkey },
113
119
  data: { timestamp: x.timestamp, prices: x.dict },
114
120
  signature: x.signature,
115
121
  }));
116
122
  const packedOracleData = packOraclesData(oraclesData, nonEmptymedianData.map(x => x.assetId));
117
-
118
123
  const dict = Dictionary.empty<bigint, bigint>();
119
124
  for (const medianDataAsset of nonEmptymedianData) {
120
125
  dict.set(medianDataAsset.assetId, medianDataAsset.medianPrice);
121
126
  }
122
-
123
127
  return {
124
128
  dict: dict,
125
129
  dataCell: packPrices(packedMedianData, packedOracleData)
126
130
  }
127
131
  }
128
132
 
129
- async #collectPrices(): Promise<boolean> {
130
- try {
131
- this.#prices = await Promise.any(this.#priceSources.map(x => collectAndFilterPrices(x, this.#poolConfig)));
132
- return true;
133
+ async #collectPrices(fetchConfig?: FetchConfig): Promise<boolean> {
134
+ for (const priceSource of this.#priceSources) {
135
+ try {
136
+ this.#prices = await proxyFetchRetries(
137
+ collectAndFilterPrices(priceSource, this.#minimalOracles, fetchConfig),
138
+ fetchConfig,
139
+ );
140
+ return true;
141
+ } catch (error) {
142
+ // Try next source
143
+ continue;
144
+ }
133
145
  }
134
- catch { }
146
+
135
147
  return false;
136
148
  }
137
149
 
138
- #filterPrices(): number { // filter again for expire check
150
+ async #collectPricesWithValidation(fetchConfig?: FetchConfig): Promise<void> {
151
+ if (!this.#prices || this.#filterPrices() < this.#minimalOracles) {
152
+ const success = await this.#collectPrices(fetchConfig);
153
+ if (!success || this.#prices.length < this.#minimalOracles) {
154
+ throw new Error(
155
+ `Failed to collect sufficient prices: ${this.#prices?.length || 0} of ${this.#minimalOracles}`,
156
+ );
157
+ }
158
+ }
159
+ }
160
+
161
+ #filterPrices(): number {
139
162
  this.#prices = this.#prices.filter(verifyPricesTimestamp());
140
163
  return this.#prices.length;
141
164
  }
142
165
 
143
166
  #filterEmptyPrincipalsAndAssets(principals: Dictionary<bigint, bigint>) {
144
- return principals.keys().filter(x => principals.get(x)! != 0n).map(x => this.#poolConfig.poolAssetsConfig.find(asset => asset.assetId == x));
167
+ return principals.keys().filter(x => principals.get(x)! != 0n).map(x => this.#poolAssetsConfig.find(asset => asset.assetId == x));
145
168
  }
146
169
  }
@@ -0,0 +1,294 @@
1
+ import { HermesClient, HexString, PriceUpdate } from '@pythnetwork/hermes-client';
2
+ import { Cell, Dictionary } from '@ton/core';
3
+ import { checkNotInDebtAtAll } from '../api/math';
4
+ import { OracleConfig } from '../api/parsers/PythOracleParser';
5
+ import { packPythUpdatesData } from '../api/prices';
6
+ import { STTON_MAINNET, TON_MAINNET, TSTON_MAINNET, TSUSDE_MAINNET, USDE_MAINNET } from '../constants';
7
+ import { FeedMapItem, parseFeedsMapDict, PoolAssetConfig, PoolAssetsConfig } from '../types/Master';
8
+ import { FetchConfig, proxyFetchRetries } from '../utils/utils';
9
+ import { Oracle } from './Oracle.interface';
10
+ import { Prices } from './Prices';
11
+ import { PythFeedUpdateType, PythPriceSourcesConfig } from './Types';
12
+ import { TTL_ORACLE_DATA_SEC } from './constants';
13
+
14
+ export type PythCollectorConfig = {
15
+ poolAssetsConfig: PoolAssetsConfig;
16
+ pythOracle: OracleConfig;
17
+ pythConfig: PythPriceSourcesConfig;
18
+ };
19
+
20
+ export class PythCollector implements Oracle {
21
+ #oracleInfo: OracleConfig;
22
+ #parsedFeedsMap: Map<bigint, FeedMapItem>;
23
+ #pythConfig: PythPriceSourcesConfig;
24
+ #poolAssetsConfig: PoolAssetsConfig;
25
+
26
+ #pythToEvaaDirect = new Map<bigint, bigint>(); // pythId -> evaaId (native)
27
+ #pythToEvaaReferred = new Map<bigint, Set<bigint>>(); // pythId -> Set<evaaId>,
28
+ #evaaToPythDirect = new Map<bigint, bigint>(); // evaaId -> pythId (native)
29
+ #allowedRefEvaa = new Map<bigint, bigint>(); // evaaId -> baseEvaaId (allowedRefTokens)
30
+
31
+ constructor(config: PythCollectorConfig) {
32
+ this.#oracleInfo = config.pythOracle;
33
+ this.#pythConfig = config.pythConfig;
34
+ this.#poolAssetsConfig = config.poolAssetsConfig;
35
+ this.#parsedFeedsMap = parseFeedsMapDict(this.#oracleInfo.feedsMap);
36
+
37
+ // 1) pythId -> evaaId, evaaId -> pythId
38
+ for (const [pythId, feedInfo] of this.#parsedFeedsMap.entries()) {
39
+ this.#pythToEvaaDirect.set(pythId, feedInfo.evaaId);
40
+ this.#evaaToPythDirect.set(feedInfo.evaaId, pythId);
41
+ }
42
+
43
+ // 2) pythId (native) -> Set<evaaId>
44
+ for (const [pythId, feedInfo] of this.#parsedFeedsMap.entries()) {
45
+ const ref = feedInfo.referredPythFeed;
46
+ if (ref && ref !== 0n) {
47
+ if (!this.#pythToEvaaReferred.has(ref)) this.#pythToEvaaReferred.set(ref, new Set());
48
+ this.#pythToEvaaReferred.get(ref)!.add(feedInfo.evaaId);
49
+ }
50
+ }
51
+
52
+ // 3) evaaId -> baseEvaaId (allowedRefTokens)
53
+ for (const evaaId of this.#oracleInfo.allowedRefTokens.keys()) {
54
+ const base = this.#oracleInfo.allowedRefTokens.get(evaaId)!;
55
+ this.#allowedRefEvaa.set(evaaId, base);
56
+
57
+ // If baseEvaaId have pythId. evaaId -> pyth(base)
58
+ const basePyth = this.#evaaToPythDirect.get(base);
59
+ if (basePyth) {
60
+ if (!this.#pythToEvaaReferred.has(basePyth)) this.#pythToEvaaReferred.set(basePyth, new Set());
61
+ this.#pythToEvaaReferred.get(basePyth)!.add(evaaId);
62
+ }
63
+ }
64
+ }
65
+
66
+ async getPricesForLiquidate(
67
+ realPrincipals: Dictionary<bigint, bigint>,
68
+ fetchConfig?: FetchConfig,
69
+ ): Promise<Prices> {
70
+ const assets = this.#filterEmptyPrincipalsAndAssets(realPrincipals);
71
+ if (assets.includes(undefined)) {
72
+ throw new Error('User from another pool');
73
+ }
74
+ return await this.getPrices(
75
+ assets.map((x) => x!),
76
+ fetchConfig,
77
+ );
78
+ }
79
+
80
+ async getPricesForSupplyWithdraw(
81
+ realPrincipals: Dictionary<bigint, bigint>,
82
+ supplyAsset: PoolAssetConfig | undefined,
83
+ withdrawAsset: PoolAssetConfig | undefined,
84
+ collateralToDebt: boolean,
85
+ fetchConfig?: FetchConfig,
86
+ ): Promise<Prices> {
87
+ let assets = this.#filterEmptyPrincipalsAndAssets(realPrincipals);
88
+ if (
89
+ checkNotInDebtAtAll(realPrincipals) &&
90
+ withdrawAsset &&
91
+ (realPrincipals.get(withdrawAsset.assetId) ?? 0n) > 0n &&
92
+ !collateralToDebt
93
+ ) {
94
+ return new Prices(Dictionary.empty<bigint, bigint>(), Cell.EMPTY, undefined, undefined);
95
+ }
96
+ if (assets.includes(undefined)) {
97
+ throw new Error('User from another pool');
98
+ }
99
+ if (withdrawAsset && !assets.find((a) => a?.assetId === withdrawAsset.assetId)) {
100
+ assets.push(withdrawAsset);
101
+ }
102
+ if (collateralToDebt && assets.length == 1) {
103
+ throw new Error('Cannot debt only one supplied asset');
104
+ }
105
+ return await this.getPrices(
106
+ assets.map((x) => x!),
107
+ fetchConfig,
108
+ );
109
+ }
110
+
111
+ async getPrices(assets: PoolAssetsConfig = this.#poolAssetsConfig, fetchConfig?: FetchConfig): Promise<Prices> {
112
+ // Declare variables at the beginning
113
+ let minPublishTime: bigint | undefined;
114
+ let maxPublishTime: bigint | undefined;
115
+
116
+ if (assets.length === 0) {
117
+ return new Prices(Dictionary.empty<bigint, bigint>(), Cell.EMPTY, undefined, undefined);
118
+ }
119
+
120
+ const requiredFeeds = this.createRequiredFeedsList(assets.map((a) => a.assetId));
121
+ const pythUpdates = await this.#fetchPythUpdatesWithRetry(requiredFeeds, fetchConfig);
122
+
123
+ // Calculate min and max publish times for validation
124
+
125
+ if (pythUpdates.parsed && pythUpdates.parsed.length > 0) {
126
+ const publishTimes = pythUpdates.parsed.map((u) => BigInt(u.price.publish_time));
127
+ const tmin = publishTimes.reduce((a, b) => (a < b ? a : b));
128
+ const tmax = publishTimes.reduce((a, b) => (a > b ? a : b));
129
+
130
+ if (tmax - tmin > TTL_ORACLE_DATA_SEC) {
131
+ throw new Error(
132
+ `Price feeds don't fit in a single 3-minute window. Time span: ${tmax - tmin} seconds (max allowed: ${TTL_ORACLE_DATA_SEC})`,
133
+ );
134
+ }
135
+
136
+ // Set boundaries using "from oldest" approach: minPublishTime = tmin, maxPublishTime = tmin + 180
137
+ minPublishTime = tmin;
138
+ maxPublishTime = tmin + BigInt(TTL_ORACLE_DATA_SEC);
139
+ }
140
+
141
+ const pricesDict = Dictionary.empty<bigint, bigint>();
142
+ const pythPriceUpdates = pythUpdates.parsed;
143
+
144
+ if (pythPriceUpdates) {
145
+ // Only set prices for requested assets, not all possible mapped assets
146
+ const requestedAssetIds = new Set(assets.map((a) => a.assetId));
147
+
148
+ for (const u of pythPriceUpdates) {
149
+ const pythId = BigInt('0x' + u.id);
150
+
151
+ const price = (BigInt(u.price.price) * BigInt(10 ** 9)) / BigInt(10 ** (u.price.expo * -1));
152
+
153
+ // Set price for direct mapping if the evaaId is requested
154
+ const directEvaa = this.#pythToEvaaDirect.get(pythId);
155
+ if (directEvaa && requestedAssetIds.has(directEvaa)) {
156
+ pricesDict.set(directEvaa, price);
157
+ }
158
+
159
+ // Set price for referred assets only if they are requested
160
+ const referredSet = this.#pythToEvaaReferred.get(pythId);
161
+ if (referredSet && referredSet.size) {
162
+ for (const evaaId of referredSet) {
163
+ if (requestedAssetIds.has(evaaId)) {
164
+ pricesDict.set(evaaId, price);
165
+ }
166
+ }
167
+ }
168
+ }
169
+
170
+ // TODO: fix it
171
+
172
+ if (pricesDict.get(TSTON_MAINNET.assetId) && pricesDict.get(TON_MAINNET.assetId)) {
173
+ pricesDict.set(
174
+ TSTON_MAINNET.assetId,
175
+ (pricesDict.get(TSTON_MAINNET.assetId)! * pricesDict.get(TON_MAINNET.assetId)!) / BigInt(10 ** 9),
176
+ );
177
+ }
178
+
179
+ // TODO: fix it
180
+ if (pricesDict.get(STTON_MAINNET.assetId) && pricesDict.get(TON_MAINNET.assetId)) {
181
+ pricesDict.set(
182
+ STTON_MAINNET.assetId,
183
+ (pricesDict.get(STTON_MAINNET.assetId)! * pricesDict.get(TON_MAINNET.assetId)!) / BigInt(10 ** 9),
184
+ );
185
+ }
186
+
187
+ // TODO: fix it
188
+ if (pricesDict.get(TSUSDE_MAINNET.assetId) && pricesDict.get(USDE_MAINNET.assetId)) {
189
+ pricesDict.set(
190
+ TSUSDE_MAINNET.assetId,
191
+ (pricesDict.get(TSUSDE_MAINNET.assetId)! * pricesDict.get(USDE_MAINNET.assetId)!) / BigInt(10 ** 9),
192
+ );
193
+ }
194
+
195
+ // Check that all requested assets have prices
196
+ const missing = assets.map((a) => a.assetId).filter((id) => pricesDict.get(id) === undefined);
197
+
198
+ if (missing.length) {
199
+ throw new Error(
200
+ `Missing prices for ${missing.length} asset(s): ${missing.map((x) => x.toString()).join(', ')}`,
201
+ );
202
+ }
203
+
204
+ const dataCell = packPythUpdatesData(pythUpdates.binary);
205
+ return new Prices(pricesDict, dataCell, minPublishTime, maxPublishTime);
206
+ }
207
+ return new Prices(Dictionary.empty<bigint, bigint>(), Cell.EMPTY, minPublishTime, maxPublishTime);
208
+ }
209
+
210
+ async getPricesForWithdraw(
211
+ realPrincipals: Dictionary<bigint, bigint>,
212
+ withdrawAsset: PoolAssetConfig,
213
+ collateralToDebt = false,
214
+ fetchConfig?: FetchConfig,
215
+ ): Promise<Prices> {
216
+ let assets = this.#filterEmptyPrincipalsAndAssets(realPrincipals);
217
+ if (
218
+ checkNotInDebtAtAll(realPrincipals) &&
219
+ (realPrincipals.get(withdrawAsset.assetId) ?? 0n) > 0n &&
220
+ !collateralToDebt
221
+ ) {
222
+ return new Prices(Dictionary.empty<bigint, bigint>(), Cell.EMPTY);
223
+ }
224
+ if (assets.includes(undefined)) {
225
+ throw new Error('User from another pool');
226
+ }
227
+ if (!assets.includes(withdrawAsset)) {
228
+ assets.push(withdrawAsset);
229
+ }
230
+ if (collateralToDebt && assets.length == 1) {
231
+ throw new Error('Cannot debt only one supplied asset');
232
+ }
233
+ return await this.getPrices(
234
+ assets.map((x) => x!),
235
+ fetchConfig,
236
+ );
237
+ }
238
+
239
+ /**
240
+ * Updates feeds data from specified endpoint
241
+ * @param feedIds list of pyth feed ids to fetch
242
+ * @returns binary - buffer of feeds update, parsed - json feeds data
243
+ */
244
+ async #getPythFeedsUpdates(feedIds: HexString[]): Promise<PythFeedUpdateType> {
245
+ const latestPriceUpdates: PriceUpdate = await Promise.any(
246
+ this.#pythConfig.pythEndpoints.map((x) =>
247
+ new HermesClient(x).getLatestPriceUpdates(feedIds, { encoding: 'hex' }),
248
+ ),
249
+ );
250
+
251
+ const parsed = latestPriceUpdates['parsed'];
252
+ const binary = Buffer.from(latestPriceUpdates.binary.data[0], 'hex');
253
+
254
+ return { binary, parsed };
255
+ }
256
+
257
+ async #fetchPythUpdatesWithRetry(
258
+ requiredFeeds: HexString[],
259
+ fetchConfig?: FetchConfig,
260
+ ): Promise<PythFeedUpdateType> {
261
+ return proxyFetchRetries(this.#getPythFeedsUpdates(requiredFeeds), fetchConfig);
262
+ }
263
+
264
+ public createRequiredFeedsList(evaaIds: bigint[]): HexString[] {
265
+ const requiredFeeds = new Set<bigint>();
266
+
267
+ for (const evaaId of evaaIds) {
268
+ let pythId = this.#evaaToPythDirect.get(evaaId);
269
+
270
+ // If evaaId no have native feed — try by allowedRefTokens (evAA->baseEvAA->pyth)
271
+ if (!pythId) {
272
+ const baseEvaa = this.#allowedRefEvaa.get(evaaId);
273
+ if (baseEvaa) pythId = this.#evaaToPythDirect.get(baseEvaa) ?? null!;
274
+ }
275
+
276
+ if (pythId) {
277
+ requiredFeeds.add(pythId);
278
+ const feedInfo = this.#parsedFeedsMap.get(pythId);
279
+ if (feedInfo?.referredPythFeed && feedInfo.referredPythFeed !== 0n) {
280
+ requiredFeeds.add(feedInfo.referredPythFeed);
281
+ }
282
+ }
283
+ }
284
+
285
+ return Array.from(requiredFeeds).map((id) => '0x' + id.toString(16));
286
+ }
287
+
288
+ #filterEmptyPrincipalsAndAssets(principals: Dictionary<bigint, bigint>) {
289
+ return principals
290
+ .keys()
291
+ .filter((x) => principals.get(x)! != 0n)
292
+ .map((x) => this.#poolAssetsConfig.find((asset) => asset.assetId == x));
293
+ }
294
+ }
@@ -1,4 +1,5 @@
1
- import { Cell, Dictionary } from '@ton/core';
1
+ import type { PriceUpdate } from '@pythnetwork/hermes-client';
2
+ import type { Cell, Dictionary } from '@ton/core';
2
3
 
3
4
  /**
4
5
  * Configuration for price source endpoints.
@@ -6,18 +7,35 @@ import { Cell, Dictionary } from '@ton/core';
6
7
  export type PriceSourcesConfig = {
7
8
  /** Endpoints for backend price data */
8
9
  backendEndpoints: string[];
9
-
10
+
10
11
  /** Endpoints for ICP price data */
11
12
  icpEndpoints: string[];
12
13
  };
13
14
 
15
+ /**
16
+ * Configuration for pyth prices.
17
+ */
18
+
19
+ export type PythPriceSourcesConfig = {
20
+ /** Endpoints for pyth price data */
21
+ pythEndpoints: string[];
22
+ };
23
+
14
24
  /**
15
25
  * Default configuration for price source endpoints.
16
26
  */
17
27
  export const DefaultPriceSourcesConfig: PriceSourcesConfig = {
18
28
  backendEndpoints: ['api.evaa.space', 'evaa.space'],
19
29
  icpEndpoints: ['6khmc-aiaaa-aaaap-ansfq-cai.raw.icp0.io'],
20
- }
30
+ };
31
+
32
+ /**
33
+ * Configuration for pyth price sources.
34
+ */
35
+ export const DefaultPythPriceSourcesConfig: PythPriceSourcesConfig = {
36
+ // FYI: 3RPS limit per IP, TODO: support Pythnet RPC
37
+ pythEndpoints: ['https://hermes.pyth.network'],
38
+ };
21
39
 
22
40
  export type RawPriceData = {
23
41
  dict: Dictionary<bigint, bigint>;
@@ -28,6 +46,10 @@ export type RawPriceData = {
28
46
  timestamp: number;
29
47
  };
30
48
 
49
+ export type PythFeedUpdateType = {
50
+ parsed: PriceUpdate['parsed'];
51
+ binary: Buffer;
52
+ };
31
53
 
32
54
  export type PriceData = {
33
55
  dict: Dictionary<bigint, bigint>;
@@ -35,6 +57,6 @@ export type PriceData = {
35
57
  };
36
58
 
37
59
  export type OraclePricesData = {
38
- timestamp: number,
39
- prices: Dictionary<bigint, bigint>
40
- }
60
+ timestamp: number;
61
+ prices: Dictionary<bigint, bigint>;
62
+ };
@@ -1,6 +1,7 @@
1
- export * from './sources';
2
- export * from './Types';
3
1
  export * from './constants';
4
- export * from './utils';
5
2
  export * from './Prices';
6
3
  export * from './PricesCollector';
4
+ export * from './PythCollector';
5
+ export * from './sources';
6
+ export * from './Types';
7
+ export * from './utils';
@@ -1,32 +1,34 @@
1
- import { beginCell, Cell, Dictionary } from "@ton/core";
2
- import { RawPriceData } from "..";
3
- import { PriceSource } from "./PriceSource";
1
+ import { beginCell, Cell, Dictionary } from '@ton/core';
2
+ import { RawPriceData } from '..';
3
+ import { DefaultFetchConfig, FetchConfig, proxyFetchRetries } from '../../utils/utils';
4
+ import { PriceSource } from './PriceSource';
4
5
 
5
6
  export class BackendPriceSource extends PriceSource {
6
7
  protected priceSourceName: string = 'BackendPriceSource';
7
8
 
8
- async getPrices(): Promise<RawPriceData[]> {
9
- const data = await this.loadOracleData();
10
- return data.map(outputData => this.parsePrices(outputData));
9
+ async getPrices(fetchConfig?: FetchConfig): Promise<RawPriceData[]> {
10
+ const data = await this.loadOracleData(fetchConfig);
11
+ return data.map((outputData) => this.parsePrices(outputData));
11
12
  }
12
13
 
13
- async loadOracleData(): Promise<OutputData[]> {
14
- let response = await fetch(`https://${this._endpoint}/api/prices`, {
14
+ async loadOracleData(fetchConfig: FetchConfig = DefaultFetchConfig): Promise<OutputData[]> {
15
+ const fetchPromise = fetch(`https://${this._endpoint}/api/prices`, {
15
16
  headers: { accept: 'application/json' },
16
- signal: AbortSignal.timeout(5000)
17
- });
17
+ signal: AbortSignal.timeout(fetchConfig.timeout),
18
+ }).then(async (response) => {
19
+ const resp = await response.json();
20
+ const data = resp as Record<string, string>;
21
+ let outputData: OutputData[] = [];
18
22
 
19
- const resp = (await response.json());
20
- const data = resp as Record<string, string>;
21
- let outputData: OutputData[] = [];
23
+ for (const nft of this._nfts) {
24
+ outputData.push({ oracleId: nft.id, data: data[nft.address] });
25
+ }
22
26
 
23
- for (const nft of this._nfts) {
24
- outputData.push({ oracleId: nft.id, data: data[nft.address] })
25
- }
26
-
27
- return outputData;
28
- }
27
+ return outputData;
28
+ });
29
29
 
30
+ return await proxyFetchRetries(fetchPromise, fetchConfig);
31
+ }
30
32
 
31
33
  parsePrices(outputData: OutputData): RawPriceData {
32
34
  try {