@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.
- package/dist/api/feeds.d.ts +30 -0
- package/dist/api/feeds.js +73 -0
- package/dist/api/liquidation.js +1 -1
- package/dist/api/math.js +1 -1
- package/dist/api/parser.d.ts +4 -2
- package/dist/api/parser.js +39 -18
- package/dist/api/parsers/AbstractOracleParser.d.ts +11 -0
- package/dist/api/parsers/AbstractOracleParser.js +9 -0
- package/dist/api/parsers/ClassicOracleParser.d.ts +10 -0
- package/dist/api/parsers/ClassicOracleParser.js +16 -0
- package/dist/api/parsers/PythOracleParser.d.ts +17 -0
- package/dist/api/parsers/PythOracleParser.js +22 -0
- package/dist/api/parsers/index.d.ts +3 -0
- package/dist/api/parsers/index.js +19 -0
- package/dist/api/prices.d.ts +6 -6
- package/dist/api/prices.js +37 -46
- package/dist/api/pyth.d.ts +16 -0
- package/dist/api/pyth.js +35 -0
- package/dist/constants/assets/assetId.d.ts +22 -0
- package/dist/constants/assets/assetId.js +29 -0
- package/dist/constants/assets/index.d.ts +3 -0
- package/dist/constants/assets/index.js +19 -0
- package/dist/constants/assets/mainnet.d.ts +19 -0
- package/dist/constants/assets/mainnet.js +114 -0
- package/dist/constants/assets/testnet.d.ts +14 -0
- package/dist/constants/assets/testnet.js +54 -0
- package/dist/constants/general/index.d.ts +65 -0
- package/dist/constants/general/index.js +93 -0
- package/dist/constants/general/mainnet.d.ts +24 -0
- package/dist/constants/general/mainnet.js +53 -0
- package/dist/constants/general/testnet.d.ts +12 -0
- package/dist/constants/general/testnet.js +15 -0
- package/dist/constants/general.d.ts +2 -2
- package/dist/constants/general.js +4 -4
- package/dist/constants/index.d.ts +3 -0
- package/dist/constants/index.js +19 -0
- package/dist/constants/pools/index.d.ts +2 -0
- package/dist/constants/pools/index.js +18 -0
- package/dist/constants/pools/mainnet.d.ts +14 -0
- package/dist/constants/pools/mainnet.js +145 -0
- package/dist/constants/pools/testnet.d.ts +9 -0
- package/dist/constants/pools/testnet.js +57 -0
- package/dist/constants/pools.js +7 -7
- package/dist/contracts/AbstractMaster.d.ts +185 -0
- package/dist/contracts/AbstractMaster.js +179 -0
- package/dist/contracts/ClassicMaster.d.ts +34 -0
- package/dist/contracts/ClassicMaster.js +87 -0
- package/dist/contracts/PythMaster.d.ts +61 -0
- package/dist/contracts/PythMaster.js +179 -0
- package/dist/contracts/UserContract.d.ts +1 -7
- package/dist/contracts/UserContract.js +1 -19
- package/dist/contracts/index.d.ts +5 -0
- package/dist/contracts/index.js +21 -0
- package/dist/index.d.ts +14 -14
- package/dist/index.js +20 -60
- package/dist/prices/Oracle.interface.d.ts +9 -0
- package/dist/prices/Oracle.interface.js +2 -0
- package/dist/prices/Prices.d.ts +5 -3
- package/dist/prices/Prices.js +13 -3
- package/dist/prices/PricesCollector.d.ts +17 -7
- package/dist/prices/PricesCollector.js +67 -51
- package/dist/prices/PythCollector.d.ts +22 -0
- package/dist/prices/PythCollector.js +217 -0
- package/dist/prices/Types.d.ts +17 -1
- package/dist/prices/Types.js +8 -1
- package/dist/prices/index.d.ts +4 -3
- package/dist/prices/index.js +4 -3
- package/dist/prices/sources/Backend.d.ts +5 -4
- package/dist/prices/sources/Backend.js +16 -13
- package/dist/prices/sources/Icp.d.ts +2 -1
- package/dist/prices/sources/Icp.js +12 -9
- package/dist/prices/sources/PriceSource.d.ts +7 -6
- package/dist/prices/utils.d.ts +10 -8
- package/dist/prices/utils.js +32 -46
- package/dist/types/Master.d.ts +10 -30
- package/dist/types/Master.js +3 -0
- package/dist/utils/userJettonWallet.js +0 -8
- package/dist/utils/utils.d.ts +8 -1
- package/dist/utils/utils.js +31 -2
- package/package.json +3 -2
- package/src/api/feeds.ts +90 -0
- package/src/api/liquidation.ts +1 -1
- package/src/api/math.ts +1 -1
- package/src/api/parser.ts +100 -38
- package/src/api/parsers/AbstractOracleParser.ts +16 -0
- package/src/api/parsers/ClassicOracleParser.ts +20 -0
- package/src/api/parsers/PythOracleParser.ts +34 -0
- package/src/api/parsers/index.ts +3 -0
- package/src/api/prices.ts +32 -41
- package/src/constants/assets/assetId.ts +30 -0
- package/src/constants/assets/index.ts +3 -0
- package/src/constants/{assets.ts → assets/mainnet.ts} +3 -96
- package/src/constants/assets/testnet.ts +74 -0
- package/src/constants/general/index.ts +91 -0
- package/src/constants/{general.ts → general/mainnet.ts} +48 -72
- package/src/constants/general/testnet.ts +25 -0
- package/src/constants/index.ts +3 -0
- package/src/constants/pools/index.ts +2 -0
- package/src/constants/pools/mainnet.ts +218 -0
- package/src/constants/pools/testnet.ts +75 -0
- package/src/contracts/AbstractMaster.ts +450 -0
- package/src/contracts/ClassicMaster.ts +149 -0
- package/src/contracts/PythMaster.ts +313 -0
- package/src/contracts/UserContract.ts +7 -28
- package/src/contracts/index.ts +7 -0
- package/src/index.ts +18 -85
- package/src/prices/Oracle.interface.ts +18 -0
- package/src/prices/Prices.ts +17 -4
- package/src/prices/PricesCollector.ts +91 -68
- package/src/prices/PythCollector.ts +294 -0
- package/src/prices/Types.ts +28 -6
- package/src/prices/index.ts +4 -3
- package/src/prices/sources/Backend.ts +21 -19
- package/src/prices/sources/Icp.ts +13 -10
- package/src/prices/sources/PriceSource.ts +6 -5
- package/src/prices/utils.ts +65 -68
- package/src/types/Master.ts +29 -52
- package/src/types/User.ts +15 -7
- package/src/utils/userJettonWallet.ts +0 -8
- package/src/utils/utils.ts +41 -2
- package/src/constants/pools.ts +0 -177
- package/src/contracts/MasterContract.ts +0 -410
|
@@ -1,146 +1,169 @@
|
|
|
1
|
-
import { Cell, Dictionary } from "@ton/core"
|
|
2
|
-
import {
|
|
3
|
-
import { PoolAssetConfig, PoolAssetsConfig
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
export
|
|
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
|
-
#
|
|
22
|
+
#poolAssetsConfig: PoolAssetsConfig;
|
|
15
23
|
#sourcesConfig: PriceSourcesConfig;
|
|
16
24
|
#priceSources: PriceSource[];
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
this.#
|
|
21
|
-
this.#
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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!),
|
|
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
|
|
63
|
-
|
|
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
|
-
|
|
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.#
|
|
82
|
-
throw new Error(`Error per updating prices, valid ${this.#prices.length} of ${this.#
|
|
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
|
-
|
|
90
|
-
|
|
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.#
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
146
|
+
|
|
135
147
|
return false;
|
|
136
148
|
}
|
|
137
149
|
|
|
138
|
-
#
|
|
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.#
|
|
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
|
+
}
|
package/src/prices/Types.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
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
|
+
};
|
package/src/prices/index.ts
CHANGED
|
@@ -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
|
|
2
|
-
import { RawPriceData } from
|
|
3
|
-
import {
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
23
|
+
for (const nft of this._nfts) {
|
|
24
|
+
outputData.push({ oracleId: nft.id, data: data[nft.address] });
|
|
25
|
+
}
|
|
22
26
|
|
|
23
|
-
|
|
24
|
-
|
|
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 {
|