@symmetry-hq/temp-v3-sdk 0.0.1
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/idl/idl.d.ts +8 -0
- package/dist/idl/idl.js +4958 -0
- package/dist/idl/types.d.ts +4955 -0
- package/dist/idl/types.js +2 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +60 -0
- package/dist/layouts/basket.d.ts +42 -0
- package/dist/layouts/basket.js +43 -0
- package/dist/layouts/config.d.ts +97 -0
- package/dist/layouts/config.js +102 -0
- package/dist/layouts/fraction.d.ts +6 -0
- package/dist/layouts/fraction.js +9 -0
- package/dist/layouts/oracle.d.ts +36 -0
- package/dist/layouts/oracle.js +37 -0
- package/dist/src/constants.d.ts +15 -0
- package/dist/src/constants.js +19 -0
- package/dist/src/idl/idl.d.ts +8 -0
- package/dist/src/idl/idl.js +4958 -0
- package/dist/src/idl/types.d.ts +4955 -0
- package/dist/src/idl/types.js +2 -0
- package/dist/src/index.d.ts +53 -0
- package/dist/src/index.js +199 -0
- package/dist/src/instructions/accounts.d.ts +27 -0
- package/dist/src/instructions/accounts.js +110 -0
- package/dist/src/instructions/auction.d.ts +7 -0
- package/dist/src/instructions/auction.js +43 -0
- package/dist/src/instructions/automation/auction.d.ts +6 -0
- package/dist/src/instructions/automation/auction.js +40 -0
- package/dist/src/instructions/automation/claimBounty.d.ts +12 -0
- package/dist/src/instructions/automation/claimBounty.js +44 -0
- package/dist/src/instructions/automation/priceUpdate.d.ts +15 -0
- package/dist/src/instructions/automation/priceUpdate.js +47 -0
- package/dist/src/instructions/automation/rebalanceSwap.d.ts +10 -0
- package/dist/src/instructions/automation/rebalanceSwap.js +42 -0
- package/dist/src/instructions/basket.d.ts +120 -0
- package/dist/src/instructions/basket.js +622 -0
- package/dist/src/instructions/bounty.d.ts +18 -0
- package/dist/src/instructions/bounty.js +81 -0
- package/dist/src/instructions/fee.d.ts +8 -0
- package/dist/src/instructions/fee.js +46 -0
- package/dist/src/instructions/globalConfig.d.ts +11 -0
- package/dist/src/instructions/globalConfig.js +43 -0
- package/dist/src/instructions/luts.d.ts +33 -0
- package/dist/src/instructions/luts.js +91 -0
- package/dist/src/instructions/management/addBounty.d.ts +7 -0
- package/dist/src/instructions/management/addBounty.js +39 -0
- package/dist/src/instructions/management/admin.d.ts +9 -0
- package/dist/src/instructions/management/admin.js +43 -0
- package/dist/src/instructions/management/claimFees.d.ts +7 -0
- package/dist/src/instructions/management/claimFees.js +47 -0
- package/dist/src/instructions/management/createBasket.d.ts +22 -0
- package/dist/src/instructions/management/createBasket.js +101 -0
- package/dist/src/instructions/management/edit.d.ts +34 -0
- package/dist/src/instructions/management/edit.js +192 -0
- package/dist/src/instructions/management/luts.d.ts +29 -0
- package/dist/src/instructions/management/luts.js +88 -0
- package/dist/src/instructions/pda.d.ts +26 -0
- package/dist/src/instructions/pda.js +110 -0
- package/dist/src/instructions/price.d.ts +17 -0
- package/dist/src/instructions/price.js +47 -0
- package/dist/src/instructions/user/deposit.d.ts +32 -0
- package/dist/src/instructions/user/deposit.js +168 -0
- package/dist/src/instructions/user/withdraw.d.ts +16 -0
- package/dist/src/instructions/user/withdraw.js +82 -0
- package/dist/src/layouts/basket.d.ts +41 -0
- package/dist/src/layouts/basket.js +43 -0
- package/dist/src/layouts/config.d.ts +133 -0
- package/dist/src/layouts/config.js +137 -0
- package/dist/src/layouts/fraction.d.ts +6 -0
- package/dist/src/layouts/fraction.js +9 -0
- package/dist/src/layouts/intents/bounty.d.ts +18 -0
- package/dist/src/layouts/intents/bounty.js +19 -0
- package/dist/src/layouts/intents/intent.d.ts +101 -0
- package/dist/src/layouts/intents/intent.js +113 -0
- package/dist/src/layouts/intents/rebalanceIntent.d.ts +56 -0
- package/dist/src/layouts/intents/rebalanceIntent.js +63 -0
- package/dist/src/layouts/lookupTable.d.ts +7 -0
- package/dist/src/layouts/lookupTable.js +10 -0
- package/dist/src/layouts/oracle.d.ts +42 -0
- package/dist/src/layouts/oracle.js +43 -0
- package/dist/src/states/basket.d.ts +8 -0
- package/dist/src/states/basket.js +54 -0
- package/dist/src/states/intents/intent.d.ts +14 -0
- package/dist/src/states/intents/intent.js +90 -0
- package/dist/src/states/intents/rebalanceIntent.d.ts +8 -0
- package/dist/src/states/intents/rebalanceIntent.js +54 -0
- package/dist/src/states/oracles/clmm_oracle.d.ts +178 -0
- package/dist/src/states/oracles/clmm_oracle.js +546 -0
- package/dist/src/states/oracles/constants.d.ts +8 -0
- package/dist/src/states/oracles/constants.js +12 -0
- package/dist/src/states/oracles/oracle.d.ts +60 -0
- package/dist/src/states/oracles/oracle.js +237 -0
- package/dist/src/states/oracles/pythOracle.d.ts +39 -0
- package/dist/src/states/oracles/pythOracle.js +202 -0
- package/dist/src/states/oracles/pyth_oracle.d.ts +39 -0
- package/dist/src/states/oracles/pyth_oracle.js +202 -0
- package/dist/src/states/oracles/raydiumClmmOracle.d.ts +178 -0
- package/dist/src/states/oracles/raydiumClmmOracle.js +546 -0
- package/dist/src/states/oracles/raydiumCpmmOracle.d.ts +139 -0
- package/dist/src/states/oracles/raydiumCpmmOracle.js +420 -0
- package/dist/src/states/oracles/raydium_cpmm_oracle.d.ts +139 -0
- package/dist/src/states/oracles/raydium_cpmm_oracle.js +420 -0
- package/dist/src/states/oracles/switchboardOracle.d.ts +0 -0
- package/dist/src/states/oracles/switchboardOracle.js +1 -0
- package/dist/src/states/oracles/switchboard_oracle.d.ts +0 -0
- package/dist/src/states/oracles/switchboard_oracle.js +1 -0
- package/dist/src/txUtils.d.ts +26 -0
- package/dist/src/txUtils.js +183 -0
- package/dist/states/basket.d.ts +8 -0
- package/dist/states/basket.js +57 -0
- package/dist/test.d.ts +1 -0
- package/dist/test.js +39 -0
- package/package.json +30 -0
- package/src/constants.ts +24 -0
- package/src/index.ts +260 -0
- package/src/instructions/automation/auction.ts +55 -0
- package/src/instructions/automation/claimBounty.ts +69 -0
- package/src/instructions/automation/priceUpdate.ts +73 -0
- package/src/instructions/automation/rebalanceSwap.ts +60 -0
- package/src/instructions/management/addBounty.ts +56 -0
- package/src/instructions/management/admin.ts +65 -0
- package/src/instructions/management/claimFees.ts +59 -0
- package/src/instructions/management/createBasket.ts +148 -0
- package/src/instructions/management/edit.ts +255 -0
- package/src/instructions/management/luts.ts +134 -0
- package/src/instructions/pda.ts +160 -0
- package/src/instructions/user/deposit.ts +237 -0
- package/src/instructions/user/withdraw.ts +130 -0
- package/src/layouts/basket.ts +82 -0
- package/src/layouts/config.ts +301 -0
- package/src/layouts/fraction.ts +12 -0
- package/src/layouts/intents/bounty.ts +35 -0
- package/src/layouts/intents/intent.ts +204 -0
- package/src/layouts/intents/rebalanceIntent.ts +111 -0
- package/src/layouts/lookupTable.ts +14 -0
- package/src/layouts/oracle.ts +96 -0
- package/src/states/basket.ts +56 -0
- package/src/states/intents/intent.ts +107 -0
- package/src/states/intents/rebalanceIntent.ts +57 -0
- package/src/states/oracles/constants.ts +13 -0
- package/src/states/oracles/oracle.ts +260 -0
- package/src/states/oracles/pythOracle.ts +270 -0
- package/src/states/oracles/raydiumClmmOracle.ts +812 -0
- package/src/states/oracles/raydiumCpmmOracle.ts +614 -0
- package/src/states/oracles/switchboardOracle.ts +0 -0
- package/src/txUtils.ts +250 -0
- package/test.ts +30 -0
- package/tsconfig.json +101 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import Decimal from 'decimal.js';
|
|
2
|
+
|
|
3
|
+
import { BN } from '@coral-xyz/anchor';
|
|
4
|
+
|
|
5
|
+
import { U32_MAX, U64_MAX } from './constants';
|
|
6
|
+
|
|
7
|
+
export enum ErrorCode {
|
|
8
|
+
NONE = 0,
|
|
9
|
+
|
|
10
|
+
// Basic Errors –––––––––––––––––––––
|
|
11
|
+
INVALID_PRICE = 1 << 0,
|
|
12
|
+
TOO_VOLATILE = 1 << 1,
|
|
13
|
+
TOO_UNCERTAIN = 1 << 2,
|
|
14
|
+
STALE = 1 << 3,
|
|
15
|
+
NOT_ENOUGH_LIQUIDITY = 1 << 4,
|
|
16
|
+
NOT_ENABLED = 1 << 5,
|
|
17
|
+
|
|
18
|
+
// Aggregation errors –––––––––––––––
|
|
19
|
+
NOT_ENOUGH_ORACLES = 1 << 6,
|
|
20
|
+
MISSING_REQUIRED_ORACLES = 1 << 7,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class OracleErrors extends Error{
|
|
24
|
+
code: ErrorCode;
|
|
25
|
+
|
|
26
|
+
constructor(code: ErrorCode, message?: string ) {
|
|
27
|
+
super(message ?? ErrorCode[code]);
|
|
28
|
+
this.name = "OracleErrors";
|
|
29
|
+
this.code = code;
|
|
30
|
+
};
|
|
31
|
+
static NONE = new OracleErrors(ErrorCode.NONE);
|
|
32
|
+
static INVALID_PRICE = new OracleErrors(ErrorCode.INVALID_PRICE);
|
|
33
|
+
static TOO_VOLATILE = new OracleErrors(ErrorCode.TOO_VOLATILE);
|
|
34
|
+
static TOO_UNCERTAIN = new OracleErrors(ErrorCode.TOO_UNCERTAIN);
|
|
35
|
+
static STALE = new OracleErrors(ErrorCode.STALE);
|
|
36
|
+
static NOT_ENOUGH_LIQUIDITY = new OracleErrors(ErrorCode.NOT_ENOUGH_LIQUIDITY);
|
|
37
|
+
static NOT_ENABLED = new OracleErrors(ErrorCode.NOT_ENABLED);
|
|
38
|
+
static NOT_ENOUGH_ORACLES = new OracleErrors(ErrorCode.NOT_ENOUGH_ORACLES);
|
|
39
|
+
static MISSING_REQUIRED_ORACLES = new OracleErrors(ErrorCode.NOT_ENABLED);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export class OraclePrice {
|
|
43
|
+
price: Decimal;
|
|
44
|
+
conf: Decimal;
|
|
45
|
+
updateTime: BN;
|
|
46
|
+
mid: Decimal;
|
|
47
|
+
low: Decimal;
|
|
48
|
+
high: Decimal;
|
|
49
|
+
|
|
50
|
+
constructor(price: Decimal, conf: Decimal, updateTime: BN){
|
|
51
|
+
this.price = new Decimal(price);
|
|
52
|
+
this.conf = new Decimal(conf);
|
|
53
|
+
this.updateTime = updateTime;
|
|
54
|
+
this.mid = this.price;
|
|
55
|
+
this.low = this.price.sub(this.conf);
|
|
56
|
+
this.high = this.price.add(this.conf);
|
|
57
|
+
|
|
58
|
+
let zero_Decimal = new Decimal(0);
|
|
59
|
+
if(this.price.lte(zero_Decimal)) throw new Error("price should be more than 0");
|
|
60
|
+
if(this.conf.lt(zero_Decimal)) throw new Error("confidence can't be negative");
|
|
61
|
+
if(this.updateTime.lte(0)) throw new Error("update time should be more than 0");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export class OracleResult {
|
|
66
|
+
price: OraclePrice | null;
|
|
67
|
+
error: OracleErrors = OracleErrors.NONE;
|
|
68
|
+
constructor(price: OraclePrice | null, error?: OracleErrors){
|
|
69
|
+
this.price = price;
|
|
70
|
+
this.error = error ?? OracleErrors.NONE;
|
|
71
|
+
}
|
|
72
|
+
ok(): boolean {
|
|
73
|
+
return this.error === OracleErrors.NONE;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export abstract class Oracle {
|
|
78
|
+
weight: number = 1;
|
|
79
|
+
confThreshBps: number = 1500;
|
|
80
|
+
stalenessThresh: number = 60;
|
|
81
|
+
volatilityThresh: Decimal = new Decimal(5);
|
|
82
|
+
minLiquidity: Decimal = new Decimal(0);
|
|
83
|
+
stalenessConfRateBps: number = 0;
|
|
84
|
+
|
|
85
|
+
constructor(weight: number, confThreshBps?: number, stalenessThresh?: number,
|
|
86
|
+
volatilityThresh?: number | Decimal, minLiquidity?: number | Decimal,
|
|
87
|
+
stalenessConfRateBps?: number){
|
|
88
|
+
this.weight = weight;
|
|
89
|
+
this.confThreshBps = confThreshBps ?? 1500;
|
|
90
|
+
this.stalenessThresh = stalenessThresh ?? 60;
|
|
91
|
+
this.volatilityThresh = new Decimal(volatilityThresh ?? 5);
|
|
92
|
+
this.minLiquidity = new Decimal(minLiquidity ?? 0);
|
|
93
|
+
this.stalenessConfRateBps = stalenessConfRateBps ?? 0;
|
|
94
|
+
|
|
95
|
+
if(!(0 <= this.weight && new BN(this.weight).lte(U32_MAX)))
|
|
96
|
+
throw Error(`Weight must be between 0 and ${U32_MAX.toString()}`);
|
|
97
|
+
if(!(0 <= this.confThreshBps && this.confThreshBps < 10_000))
|
|
98
|
+
throw Error(`Confidence threshold must be between 0 and 10000`);
|
|
99
|
+
if(!(0 <= this.stalenessConfRateBps && new BN(this.stalenessConfRateBps).lt(U32_MAX)))
|
|
100
|
+
throw Error(`Staleness confidence rate must be between 0 and ${U32_MAX.toString()}`);
|
|
101
|
+
if(!(new Decimal(1).lte(this.volatilityThresh) && this.volatilityThresh.lte(new Decimal(U32_MAX.toString()))))
|
|
102
|
+
throw Error(`Volatility threshold must be between 1 and ${U32_MAX.toString()}`);
|
|
103
|
+
if(!(new Decimal(0).lte(this.minLiquidity) && this.minLiquidity.lte(new Decimal(U64_MAX.toString()))))
|
|
104
|
+
throw Error(`Minimum liquidity must be between 0 and ${U64_MAX.toString()}`);
|
|
105
|
+
if(!(0 <= this.stalenessThresh && this.stalenessThresh <= 10_000))
|
|
106
|
+
throw Error(`Staleness threshold must be between 0 and 10000`);
|
|
107
|
+
|
|
108
|
+
}
|
|
109
|
+
abstract fetch(): Promise<OracleResult>;
|
|
110
|
+
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export class OracleAggregator{
|
|
114
|
+
oracles: Oracle[];
|
|
115
|
+
requiredOracles: Oracle[];
|
|
116
|
+
minConfBps: number = 0;
|
|
117
|
+
confThreshBps: number = 0;
|
|
118
|
+
minOraclesThresh: number = 0;
|
|
119
|
+
|
|
120
|
+
constructor(oracles: Oracle[], requiredOracles: Oracle[], minConfBps: number = 0,
|
|
121
|
+
confThreshBps: number = 0, minOraclesThresh: number = 0){
|
|
122
|
+
this.oracles = oracles;
|
|
123
|
+
this.requiredOracles = requiredOracles;
|
|
124
|
+
this.minConfBps = minConfBps;
|
|
125
|
+
this.confThreshBps = confThreshBps;
|
|
126
|
+
this.minOraclesThresh = minOraclesThresh;
|
|
127
|
+
|
|
128
|
+
if(!(this.oracles.length > 0))
|
|
129
|
+
throw new Error(`At least one oracle is required`);
|
|
130
|
+
if(!(0 <= this.minConfBps && this.minConfBps < 10_000))
|
|
131
|
+
throw new Error(`Minimum confidence must be between 0 and 10000`);
|
|
132
|
+
if(!(0 <= this.confThreshBps && this.confThreshBps < 10_000))
|
|
133
|
+
throw new Error(`Confidence threshold must be between 0 and 10000`);
|
|
134
|
+
if(!(0 <= this.minOraclesThresh && new BN(this.minOraclesThresh).lte(U32_MAX)))
|
|
135
|
+
throw new Error(`Minimum number of oracles must be between 0 and ${U32_MAX}`);
|
|
136
|
+
}
|
|
137
|
+
async fetch(): Promise<OracleResult> {
|
|
138
|
+
console.log("Fetching price from oracle aggregator");
|
|
139
|
+
let prices: (OraclePrice | null)[] = Array(0);
|
|
140
|
+
let weights: Decimal[] = Array(0);
|
|
141
|
+
|
|
142
|
+
for(let i = 0; i< this.oracles.length; i++){
|
|
143
|
+
try {
|
|
144
|
+
let result = await this.oracles[i].fetch();
|
|
145
|
+
if(result.ok()){
|
|
146
|
+
prices.push(result.price);
|
|
147
|
+
weights.push(new Decimal(this.oracles[i].weight));
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
if(this.requiredOracles.includes(this.oracles[i])){
|
|
151
|
+
return new OracleResult(null, OracleErrors.MISSING_REQUIRED_ORACLES);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
throw new Error("Unexpected error occured while fetching prices");
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if(prices.length <= this.minOraclesThresh)
|
|
160
|
+
return new OracleResult(null, OracleErrors.NOT_ENOUGH_ORACLES);
|
|
161
|
+
|
|
162
|
+
let allPrices: (Decimal | null)[] = Array(0);
|
|
163
|
+
let allWeights: Decimal[] = Array(0);
|
|
164
|
+
let frac3: Decimal = new Decimal(3);
|
|
165
|
+
prices.map((price, i) => {
|
|
166
|
+
allPrices.push(price?.mid ?? null , price?.low ?? null , price?.high ?? null);
|
|
167
|
+
let w = weights[i].div(frac3);
|
|
168
|
+
allWeights.push(w,w,w);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
let medianPrice = weightedMedian(allPrices, allWeights);
|
|
172
|
+
let p25Price = weightedPercentile(allPrices, allWeights, 25);
|
|
173
|
+
let p75Price = weightedPercentile(allPrices, allWeights, 75);
|
|
174
|
+
|
|
175
|
+
console.log("Median price: ", medianPrice.toString());
|
|
176
|
+
console.log("P25 price: ", p25Price.toString());
|
|
177
|
+
console.log("P75 price: ", p75Price.toString());
|
|
178
|
+
|
|
179
|
+
let first: Decimal = medianPrice.sub(p25Price);
|
|
180
|
+
let second: Decimal = p75Price.sub(medianPrice);
|
|
181
|
+
let conf = first.gte(second) ? first : second;
|
|
182
|
+
conf = conf.lte(medianPrice) ? conf : medianPrice;
|
|
183
|
+
if(conf.div(medianPrice).lt(this.minConfBps)) {
|
|
184
|
+
conf = medianPrice.mul(this.minConfBps)
|
|
185
|
+
};
|
|
186
|
+
if (conf.div(medianPrice).gt(this.confThreshBps)){
|
|
187
|
+
return new OracleResult(null, OracleErrors.TOO_UNCERTAIN);
|
|
188
|
+
}
|
|
189
|
+
const times = prices.filter((p): p is OraclePrice => p !== null) // drop nulls
|
|
190
|
+
.map(p => p.updateTime);
|
|
191
|
+
|
|
192
|
+
let oldestPriceTime: number = Math.min(...times);
|
|
193
|
+
return new OracleResult(new OraclePrice(medianPrice, conf, oldestPriceTime))
|
|
194
|
+
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function weightedMedian(values: (Decimal | null)[], weights: Decimal[]): Decimal{
|
|
199
|
+
return weightedPercentile(values, weights=weights, 50);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function weightedPercentile(values: (Decimal | null)[], weights: Decimal[], percentile: number): Decimal {
|
|
203
|
+
if (percentile < 0 || percentile > 100) {
|
|
204
|
+
throw new Error("Percentile must be between 0 and 100");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (!values || values.length === 0) {
|
|
208
|
+
throw new Error("Prices list is empty");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (weights.length !== values.length) {
|
|
212
|
+
throw new Error("Weights and prices must have the same length");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (weights.some((w) => w.lt(0))) {
|
|
216
|
+
throw new Error("Weights must be non-negative");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (weights.every((w) => w.eq(0))) {
|
|
220
|
+
throw new Error("Weights must be non-zero");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Filter out null prices along with their weights
|
|
224
|
+
const arr: { price: Decimal; weight: Decimal }[] = [];
|
|
225
|
+
for (let i = 0; i < values.length; i++) {
|
|
226
|
+
const p = values[i];
|
|
227
|
+
if (p !== null) {
|
|
228
|
+
arr.push({ price: p, weight: weights[i] });
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (arr.length === 0) {
|
|
233
|
+
throw new Error("All prices are null");
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Sort by price ascending
|
|
237
|
+
arr.sort((a, b) => a.price.sub(b.price).toNumber());
|
|
238
|
+
|
|
239
|
+
// Edge cases
|
|
240
|
+
if (percentile === 0) return arr[0].price;
|
|
241
|
+
if (percentile === 100) return arr[arr.length - 1].price;
|
|
242
|
+
|
|
243
|
+
// Compute total weight
|
|
244
|
+
const totalWeight = arr.reduce((sum, x) => sum.add(x.weight), new Decimal(0));
|
|
245
|
+
|
|
246
|
+
// Threshold = percentile% of total weight
|
|
247
|
+
const thresh = totalWeight.mul(new Decimal(percentile));
|
|
248
|
+
|
|
249
|
+
// Cumulative sum
|
|
250
|
+
let cum = new Decimal(0);
|
|
251
|
+
for (const { price, weight } of arr) {
|
|
252
|
+
cum = cum.add(weight);
|
|
253
|
+
if (cum.gte(thresh)) {
|
|
254
|
+
return price; // nearest-rank percentile
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// fallback (should not happen, but safe)
|
|
259
|
+
return arr[arr.length - 1].price;
|
|
260
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { Decimal } from 'decimal.js';
|
|
2
|
+
|
|
3
|
+
import { AnchorProvider, BN, Provider, Wallet } from '@coral-xyz/anchor';
|
|
4
|
+
import { HermesClient } from '@pythnetwork/hermes-client';
|
|
5
|
+
import { PythSolanaReceiver } from '@pythnetwork/pyth-solana-receiver';
|
|
6
|
+
import {
|
|
7
|
+
Connection, PublicKey, Signer, TransactionSignature, VersionedTransaction
|
|
8
|
+
} from '@solana/web3.js';
|
|
9
|
+
|
|
10
|
+
import { HERMES_PUBLIC_ENDPOINT, SOLANA_DEVNET_ENDPOINT } from './constants';
|
|
11
|
+
import { ErrorCode, Oracle, OracleErrors, OraclePrice, OracleResult } from './oracle';
|
|
12
|
+
|
|
13
|
+
interface PythState {
|
|
14
|
+
price: BN, // i64,
|
|
15
|
+
conf: BN, // u64,
|
|
16
|
+
expo: number, // i32,
|
|
17
|
+
publishTime: BN, // i64
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
export class PythOracle extends Oracle {
|
|
22
|
+
priceFeedAccount: PublicKey;
|
|
23
|
+
state: PythState;
|
|
24
|
+
connection: Connection;
|
|
25
|
+
priceClient: HermesClient;
|
|
26
|
+
priceFeedId: string = "";
|
|
27
|
+
|
|
28
|
+
private constructor(
|
|
29
|
+
priceFeedAccount: PublicKey,
|
|
30
|
+
connection: Connection,
|
|
31
|
+
priceClient: HermesClient,
|
|
32
|
+
state: PythState,
|
|
33
|
+
weight: number,
|
|
34
|
+
confThreshBps?: number,
|
|
35
|
+
stalenessThresh?: number,
|
|
36
|
+
volatilityThresh?: Decimal,
|
|
37
|
+
minLiquidity?: Decimal,
|
|
38
|
+
stalenessConfRateBps?: number
|
|
39
|
+
){
|
|
40
|
+
super(weight, confThreshBps, stalenessThresh, volatilityThresh, minLiquidity, stalenessConfRateBps);
|
|
41
|
+
this.priceFeedAccount = priceFeedAccount;
|
|
42
|
+
this.connection = connection;
|
|
43
|
+
this.priceClient = priceClient;
|
|
44
|
+
this.state = state;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
static async create(
|
|
48
|
+
connection: Connection = new Connection(SOLANA_DEVNET_ENDPOINT),
|
|
49
|
+
priceFeedAccount: PublicKey,
|
|
50
|
+
weight: number,
|
|
51
|
+
priceClient?: HermesClient,
|
|
52
|
+
confThreshBps?: number,
|
|
53
|
+
stalenessThresh?: number,
|
|
54
|
+
volatilityThresh?: Decimal,
|
|
55
|
+
minLiquidity?: Decimal,
|
|
56
|
+
stalenessConfRateBps?: number
|
|
57
|
+
): Promise<PythOracle> {
|
|
58
|
+
|
|
59
|
+
priceClient = priceClient ?? new HermesClient(HERMES_PUBLIC_ENDPOINT, {});
|
|
60
|
+
const priceAccountInfo = await connection.getAccountInfo(priceFeedAccount);
|
|
61
|
+
if(!priceAccountInfo) {
|
|
62
|
+
throw new Error(`Failed to fetch price account info for ${priceFeedAccount.toBase58()}`);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
let buffer: Buffer = priceAccountInfo!.data;
|
|
66
|
+
let offset = 8;
|
|
67
|
+
let writeAuthority = new PublicKey(buffer.slice(offset, offset+32));
|
|
68
|
+
offset += 32;
|
|
69
|
+
let {level, size} = parseVerificationLevel(buffer, offset);
|
|
70
|
+
offset += size;
|
|
71
|
+
let priceMessage = new PublicKey(buffer.slice(offset, offset+32));
|
|
72
|
+
offset += 32;
|
|
73
|
+
let price = new BN(buffer.slice(offset, offset+8), 'le').toString();
|
|
74
|
+
offset += 8;
|
|
75
|
+
let conf = new BN(buffer.slice(offset, offset+8), 'le').toString();
|
|
76
|
+
offset += 8;
|
|
77
|
+
let exp = new BN(buffer.slice(offset, offset + 4), 'le').fromTwos(32).toString();
|
|
78
|
+
offset += 4;
|
|
79
|
+
let publishTime = new BN(buffer.slice(offset, offset+8), 'le').fromTwos(64).toString();
|
|
80
|
+
offset += 8;
|
|
81
|
+
let state: PythState = {
|
|
82
|
+
price: new BN(price),
|
|
83
|
+
conf: new BN(conf),
|
|
84
|
+
expo: parseInt(exp),
|
|
85
|
+
publishTime: new BN(publishTime)
|
|
86
|
+
};
|
|
87
|
+
return new PythOracle(
|
|
88
|
+
// priceFeedId,
|
|
89
|
+
priceFeedAccount,
|
|
90
|
+
connection,
|
|
91
|
+
priceClient,
|
|
92
|
+
state,
|
|
93
|
+
weight,
|
|
94
|
+
confThreshBps,
|
|
95
|
+
stalenessThresh,
|
|
96
|
+
volatilityThresh,
|
|
97
|
+
minLiquidity,
|
|
98
|
+
stalenessConfRateBps
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
static async createWithId(
|
|
104
|
+
connection: Connection = new Connection(SOLANA_DEVNET_ENDPOINT),
|
|
105
|
+
priceFeedId: string,
|
|
106
|
+
wallet: Wallet,
|
|
107
|
+
weight: number,
|
|
108
|
+
priceClient?: HermesClient,
|
|
109
|
+
confThreshBps?: number,
|
|
110
|
+
stalenessThresh?: number,
|
|
111
|
+
volatilityThresh?: Decimal,
|
|
112
|
+
minLiquidity?: Decimal,
|
|
113
|
+
stalenessConfRateBps?: number
|
|
114
|
+
): Promise<PythOracle> {
|
|
115
|
+
|
|
116
|
+
const pythSolanaReceiver = new PythSolanaReceiver({
|
|
117
|
+
connection,
|
|
118
|
+
wallet,
|
|
119
|
+
});
|
|
120
|
+
priceClient = priceClient ?? new HermesClient(HERMES_PUBLIC_ENDPOINT, {});
|
|
121
|
+
const priceAccount = pythSolanaReceiver.getPriceFeedAccountAddress(1, priceFeedId);
|
|
122
|
+
const priceAccountInfo = await connection.getAccountInfo(priceAccount);
|
|
123
|
+
if(!priceAccountInfo) {
|
|
124
|
+
throw new Error(`Failed to fetch price account info for ${priceAccount.toBase58()}`);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
let buffer: Buffer = priceAccountInfo!.data;
|
|
128
|
+
let offset = 8;
|
|
129
|
+
let writeAuthority = new PublicKey(buffer.slice(offset, offset+32));
|
|
130
|
+
offset += 32;
|
|
131
|
+
let {level, size} = parseVerificationLevel(buffer, offset);
|
|
132
|
+
offset += size;
|
|
133
|
+
let priceMessage = new PublicKey(buffer.slice(offset, offset+32));
|
|
134
|
+
offset += 32;
|
|
135
|
+
let price = new BN(buffer.slice(offset, offset+8), 'le').toString();
|
|
136
|
+
offset += 8;
|
|
137
|
+
let conf = new BN(buffer.slice(offset, offset+8), 'le').toString();
|
|
138
|
+
offset += 8;
|
|
139
|
+
let exp = new BN(buffer.slice(offset, offset + 4), 'le').fromTwos(32).toString();
|
|
140
|
+
offset += 4;
|
|
141
|
+
let publishTime = new BN(buffer.slice(offset, offset+8), 'le').fromTwos(64).toString();
|
|
142
|
+
offset += 8;
|
|
143
|
+
let state: PythState = {
|
|
144
|
+
price: new BN(price),
|
|
145
|
+
conf: new BN(conf),
|
|
146
|
+
expo: parseInt(exp),
|
|
147
|
+
publishTime: new BN(publishTime)
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
let pythOracle = new PythOracle(
|
|
153
|
+
priceAccount,
|
|
154
|
+
connection,
|
|
155
|
+
priceClient,
|
|
156
|
+
state,
|
|
157
|
+
weight,
|
|
158
|
+
confThreshBps,
|
|
159
|
+
stalenessThresh,
|
|
160
|
+
volatilityThresh,
|
|
161
|
+
minLiquidity,
|
|
162
|
+
stalenessConfRateBps
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
pythOracle.setPriceFeedId(priceFeedId);
|
|
166
|
+
|
|
167
|
+
return pythOracle;
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
getConfidence(): Decimal {
|
|
172
|
+
return new Decimal(this.state.conf.toString()).mul(10**this.state.expo);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
setPriceFeedId(priceFeedId: string) {
|
|
176
|
+
this.priceFeedId = priceFeedId;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async fetch(): Promise<OracleResult> {
|
|
180
|
+
const currentTime = new BN(Math.floor(Date.now() / 1000));
|
|
181
|
+
if(currentTime.sub(this.state.publishTime).gt(new BN(this.stalenessThresh))) {
|
|
182
|
+
this.state = await this.fetchPriceFromHermes(this.priceFeedId);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
let errors = 0;
|
|
186
|
+
console.log(this.confThreshBps)
|
|
187
|
+
console.log(this.state.conf.toString());
|
|
188
|
+
let pr = new Decimal(this.state.price.toString()).mul(10**this.state.expo);
|
|
189
|
+
let cf = new Decimal(this.state.conf.toString()).mul(10**this.state.expo);
|
|
190
|
+
console.log(cf.div(pr).mul(10000));
|
|
191
|
+
if(this.getConfidence().div(this.state.price.toString()).mul(10000).gt(this.confThreshBps)) {
|
|
192
|
+
errors |= ErrorCode.TOO_UNCERTAIN;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
let err = new OracleErrors(errors);
|
|
196
|
+
console.log(this.state.publishTime);
|
|
197
|
+
let oraclePrice = new OraclePrice(
|
|
198
|
+
new Decimal(this.state.price.toString()).mul(10**this.state.expo),
|
|
199
|
+
new Decimal(this.state.conf.toString()).mul(10**this.state.expo),
|
|
200
|
+
this.state.publishTime
|
|
201
|
+
)
|
|
202
|
+
return new OracleResult(oraclePrice, err);
|
|
203
|
+
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async fetchPriceFromHermes(priceFeedId: string): Promise<PythState> {
|
|
207
|
+
const priceUpdate = await this.priceClient.getLatestPriceUpdates([priceFeedId], {encoding: "base64"});
|
|
208
|
+
if(priceUpdate && priceUpdate.parsed && priceUpdate.parsed.length > 0) {
|
|
209
|
+
const priceData = priceUpdate.parsed[0].price;
|
|
210
|
+
console.log(priceData.publishTime);
|
|
211
|
+
return {
|
|
212
|
+
price: new BN(priceData.price),
|
|
213
|
+
conf: new BN(priceData.conf),
|
|
214
|
+
expo: priceData.expo,
|
|
215
|
+
publishTime: new BN(priceData.publishTime)
|
|
216
|
+
};
|
|
217
|
+
} else {
|
|
218
|
+
throw new Error(`Failed to fetch price update from Hermes for ${priceFeedId}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Get parsed baskets by manager. Uses 10 paralel rpc calls so might get heavy. Should set feedDd before running this
|
|
223
|
+
* @param {number} shardId - between 0-2^64, default is 0
|
|
224
|
+
* @param {number} [computeUnitPrice] - compute unit price in microlamports, default is 100_000
|
|
225
|
+
*/
|
|
226
|
+
async updateFeedTx(wallet: Wallet, shardId: number = 0, computeUnitPrice: number = 100000) {
|
|
227
|
+
if(!shardId) shardId = 0;
|
|
228
|
+
if(!computeUnitPrice) computeUnitPrice = 100000;
|
|
229
|
+
let priceUpdate = await this.priceClient.getLatestPriceUpdates([this.priceFeedId], {encoding: "base64"});
|
|
230
|
+
const pythSolanaReceiver = new PythSolanaReceiver({
|
|
231
|
+
connection: this.connection,
|
|
232
|
+
wallet: wallet,
|
|
233
|
+
});
|
|
234
|
+
const transactionBuilder = pythSolanaReceiver.newTransactionBuilder({
|
|
235
|
+
// closeUpdateAccounts: false
|
|
236
|
+
});
|
|
237
|
+
await transactionBuilder.addUpdatePriceFeed(priceUpdate.binary.data, shardId);
|
|
238
|
+
let build = await transactionBuilder.buildVersionedTransactions({
|
|
239
|
+
computeUnitPriceMicroLamports: computeUnitPrice,
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
return build;
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
async sendUpdateTx(
|
|
246
|
+
provider: AnchorProvider,
|
|
247
|
+
updateFeedTx: {
|
|
248
|
+
tx: VersionedTransaction;
|
|
249
|
+
signers: Signer[];
|
|
250
|
+
}[]
|
|
251
|
+
): Promise<TransactionSignature[]>{
|
|
252
|
+
|
|
253
|
+
let txs = await provider.sendAll(updateFeedTx, { skipPreflight: true, commitment: "confirmed"});
|
|
254
|
+
return txs;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function parseVerificationLevel(buf: Buffer, offset: number): { level: string, size: number } {
|
|
261
|
+
const discr = buf.readUInt8(offset);
|
|
262
|
+
if (discr === 0) { // Partial
|
|
263
|
+
const numSignatures = buf.readUInt8(offset + 1);
|
|
264
|
+
return { level: `Partial(${numSignatures})`, size: 2 };
|
|
265
|
+
} else if (discr === 1) { // Full
|
|
266
|
+
return { level: "Full", size: 1 };
|
|
267
|
+
} else {
|
|
268
|
+
throw new Error(`Unknown verification level: ${discr}`);
|
|
269
|
+
}
|
|
270
|
+
}
|