@symmetry-hq/sdk 1.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/src/constants.d.ts +23 -0
- package/dist/src/constants.js +38 -0
- package/dist/src/index.d.ts +804 -0
- package/dist/src/index.js +2097 -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/flashSwap.d.ts +21 -0
- package/dist/src/instructions/automation/flashSwap.js +74 -0
- package/dist/src/instructions/automation/priceUpdate.d.ts +19 -0
- package/dist/src/instructions/automation/priceUpdate.js +89 -0
- package/dist/src/instructions/automation/rebalanceIntent.d.ts +32 -0
- package/dist/src/instructions/automation/rebalanceIntent.js +117 -0
- package/dist/src/instructions/automation/rebalanceSwap.d.ts +11 -0
- package/dist/src/instructions/automation/rebalanceSwap.js +42 -0
- package/dist/src/instructions/management/addBounty.d.ts +7 -0
- package/dist/src/instructions/management/addBounty.js +41 -0
- package/dist/src/instructions/management/admin.d.ts +9 -0
- package/dist/src/instructions/management/admin.js +53 -0
- package/dist/src/instructions/management/claimFees.d.ts +15 -0
- package/dist/src/instructions/management/claimFees.js +95 -0
- package/dist/src/instructions/management/createBasket.d.ts +21 -0
- package/dist/src/instructions/management/createBasket.js +98 -0
- package/dist/src/instructions/management/edit.d.ts +51 -0
- package/dist/src/instructions/management/edit.js +477 -0
- package/dist/src/instructions/management/luts.d.ts +30 -0
- package/dist/src/instructions/management/luts.js +99 -0
- package/dist/src/instructions/pda.d.ts +25 -0
- package/dist/src/instructions/pda.js +128 -0
- package/dist/src/instructions/user/deposit.d.ts +20 -0
- package/dist/src/instructions/user/deposit.js +100 -0
- package/dist/src/instructions/user/withdraw.d.ts +8 -0
- package/dist/src/instructions/user/withdraw.js +36 -0
- package/dist/src/jup.d.ts +49 -0
- package/dist/src/jup.js +80 -0
- package/dist/src/keeperMonitor.d.ts +52 -0
- package/dist/src/keeperMonitor.js +624 -0
- package/dist/src/layouts/basket.d.ts +191 -0
- package/dist/src/layouts/basket.js +51 -0
- package/dist/src/layouts/config.d.ts +281 -0
- package/dist/src/layouts/config.js +237 -0
- package/dist/src/layouts/fraction.d.ts +20 -0
- package/dist/src/layouts/fraction.js +164 -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 +209 -0
- package/dist/src/layouts/intents/intent.js +97 -0
- package/dist/src/layouts/intents/rebalanceIntent.d.ts +212 -0
- package/dist/src/layouts/intents/rebalanceIntent.js +94 -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 +63 -0
- package/dist/src/layouts/oracle.js +96 -0
- package/dist/src/states/basket.d.ts +14 -0
- package/dist/src/states/basket.js +479 -0
- package/dist/src/states/config.d.ts +3 -0
- package/dist/src/states/config.js +71 -0
- package/dist/src/states/intents/intent.d.ts +10 -0
- package/dist/src/states/intents/intent.js +316 -0
- package/dist/src/states/intents/rebalanceIntent.d.ts +42 -0
- package/dist/src/states/intents/rebalanceIntent.js +680 -0
- package/dist/src/states/oracles/constants.d.ts +9 -0
- package/dist/src/states/oracles/constants.js +15 -0
- package/dist/src/states/oracles/oracle.d.ts +24 -0
- package/dist/src/states/oracles/oracle.js +168 -0
- package/dist/src/states/oracles/pythOracle.d.ts +132 -0
- package/dist/src/states/oracles/pythOracle.js +609 -0
- package/dist/src/states/oracles/raydiumClmmOracle.d.ts +184 -0
- package/dist/src/states/oracles/raydiumClmmOracle.js +843 -0
- package/dist/src/states/oracles/raydiumCpmmOracle.d.ts +120 -0
- package/dist/src/states/oracles/raydiumCpmmOracle.js +540 -0
- package/dist/src/states/oracles/switchboardOracle.d.ts +0 -0
- package/dist/src/states/oracles/switchboardOracle.js +1 -0
- package/dist/src/states/withdrawBasketFees.d.ts +10 -0
- package/dist/src/states/withdrawBasketFees.js +154 -0
- package/dist/src/txUtils.d.ts +65 -0
- package/dist/src/txUtils.js +306 -0
- package/dist/test.d.ts +1 -0
- package/dist/test.js +561 -0
- package/package.json +31 -0
- package/src/constants.ts +40 -0
- package/src/index.ts +2431 -0
- package/src/instructions/automation/auction.ts +55 -0
- package/src/instructions/automation/claimBounty.ts +69 -0
- package/src/instructions/automation/flashSwap.ts +104 -0
- package/src/instructions/automation/priceUpdate.ts +117 -0
- package/src/instructions/automation/rebalanceIntent.ts +181 -0
- package/src/instructions/management/addBounty.ts +55 -0
- package/src/instructions/management/admin.ts +72 -0
- package/src/instructions/management/claimFees.ts +129 -0
- package/src/instructions/management/createBasket.ts +138 -0
- package/src/instructions/management/edit.ts +602 -0
- package/src/instructions/management/luts.ts +157 -0
- package/src/instructions/pda.ts +151 -0
- package/src/instructions/user/deposit.ts +143 -0
- package/src/instructions/user/withdraw.ts +53 -0
- package/src/jup.ts +113 -0
- package/src/keeperMonitor.ts +585 -0
- package/src/layouts/basket.ts +233 -0
- package/src/layouts/config.ts +576 -0
- package/src/layouts/fraction.ts +164 -0
- package/src/layouts/intents/bounty.ts +35 -0
- package/src/layouts/intents/intent.ts +324 -0
- package/src/layouts/intents/rebalanceIntent.ts +306 -0
- package/src/layouts/lookupTable.ts +14 -0
- package/src/layouts/oracle.ts +157 -0
- package/src/states/basket.ts +527 -0
- package/src/states/config.ts +62 -0
- package/src/states/intents/intent.ts +311 -0
- package/src/states/intents/rebalanceIntent.ts +751 -0
- package/src/states/oracles/constants.ts +13 -0
- package/src/states/oracles/oracle.ts +212 -0
- package/src/states/oracles/pythOracle.ts +874 -0
- package/src/states/oracles/raydiumClmmOracle.ts +1193 -0
- package/src/states/oracles/raydiumCpmmOracle.ts +784 -0
- package/src/states/oracles/switchboardOracle.ts +0 -0
- package/src/states/withdrawBasketFees.ts +160 -0
- package/src/txUtils.ts +424 -0
- package/test.ts +609 -0
- package/tsconfig.json +101 -0
|
@@ -0,0 +1,874 @@
|
|
|
1
|
+
import Decimal from 'decimal.js';
|
|
2
|
+
|
|
3
|
+
import BN from 'bn.js';
|
|
4
|
+
import { HermesClient } from '@pythnetwork/hermes-client';
|
|
5
|
+
import {
|
|
6
|
+
AccountInfo, ComputeBudgetProgram, Connection, Keypair, PACKET_DATA_SIZE, PublicKey, Signer,
|
|
7
|
+
SystemProgram, Transaction, TransactionInstruction, TransactionMessage, VersionedTransaction
|
|
8
|
+
} from '@solana/web3.js';
|
|
9
|
+
|
|
10
|
+
import { HUNDRED_PERCENT_BPS } from '../../constants';
|
|
11
|
+
import { OracleSettings } from '../../layouts/oracle';
|
|
12
|
+
import { TxBatchData, TxData } from '../../txUtils';
|
|
13
|
+
import { HERMES_PUBLIC_ENDPOINT, SOLANA_DEVNET_ENDPOINT } from './constants';
|
|
14
|
+
import { OraclePrice } from './oracle';
|
|
15
|
+
|
|
16
|
+
export type VerificationLevel =
|
|
17
|
+
| { kind: 'Partial'; numSignatures: number }
|
|
18
|
+
| { kind: 'Full' };
|
|
19
|
+
|
|
20
|
+
export function parseVerificationLevel(buf: Buffer, offset: number): { level: VerificationLevel, size: number } {
|
|
21
|
+
const discr = buf.readUInt8(offset);
|
|
22
|
+
if (discr === 0) { // Partial
|
|
23
|
+
const numSignatures = buf.readUInt8(offset + 1);
|
|
24
|
+
return { level: { kind: 'Partial', numSignatures }, size: 2 };
|
|
25
|
+
} else if (discr === 1) { // Full
|
|
26
|
+
return { level: { kind: 'Full' }, size: 1 };
|
|
27
|
+
} else {
|
|
28
|
+
throw new Error(`Unknown verification level: ${discr}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Pyth program IDs (mainnet)
|
|
33
|
+
const DEFAULT_RECEIVER_PROGRAM_ID = new PublicKey("rec5EKMGg6MxZYaMdyBfgwp4d5rB9T1VQH5pJv5LtFJ");
|
|
34
|
+
const DEFAULT_PUSH_ORACLE_PROGRAM_ID = new PublicKey("pythWSnswVUd12oZpeFP8e9CVaEqJg25g1Vtc2biRsT");
|
|
35
|
+
const DEFAULT_WORMHOLE_PROGRAM_ID = new PublicKey("HDwcJBJXjL9FpJ7UBsYBtaDjsBUhuLCUYoz3zr8SWWaQ");
|
|
36
|
+
|
|
37
|
+
// Constants from Pyth SDK vaa.js
|
|
38
|
+
const VAA_START = 46;
|
|
39
|
+
const VAA_SPLIT_INDEX = 721;
|
|
40
|
+
const VAA_SIGNATURE_SIZE = 66;
|
|
41
|
+
const DEFAULT_REDUCED_GUARDIAN_SET_SIZE = 5;
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
// Compute budget constants
|
|
45
|
+
const VERIFY_ENCODED_VAA_COMPUTE_BUDGET = 350_000;
|
|
46
|
+
const UPDATE_PRICE_FEED_COMPUTE_BUDGET = 55_000;
|
|
47
|
+
const INIT_ENCODED_VAA_COMPUTE_BUDGET = 3000;
|
|
48
|
+
const WRITE_ENCODED_VAA_COMPUTE_BUDGET = 3000;
|
|
49
|
+
const CLOSE_ENCODED_VAA_COMPUTE_BUDGET = 30_000;
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
// Pre-computed discriminators
|
|
53
|
+
const DISCRIMINATORS = {
|
|
54
|
+
// Wormhole instructions
|
|
55
|
+
initEncodedVaa: Buffer.from([209, 193, 173, 25, 91, 202, 181, 218]),
|
|
56
|
+
writeEncodedVaa: Buffer.from([199, 208, 110, 177, 150, 76, 118, 42]),
|
|
57
|
+
verifyEncodedVaaV1: Buffer.from([103, 56, 177, 229, 240, 103, 68, 73]),
|
|
58
|
+
closeEncodedVaa: Buffer.from([48, 221, 174, 198,231, 7, 152, 38]),
|
|
59
|
+
// Pyth Push Oracle instructions
|
|
60
|
+
updatePriceFeed: Buffer.from([28, 9, 93, 150, 86, 153, 188, 115]),
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// Jito tip accounts (copied from pyth solana-utils)
|
|
64
|
+
export const TIP_ACCOUNTS = [
|
|
65
|
+
"HFqU5x63VTqvQss8hp11i4wVV8bD44PvwucfZ2bU7gRe",
|
|
66
|
+
"Cw8CFyM9FkoMi7K7Crf6HNQqf4uEMzpKw6QNghXLvLkY",
|
|
67
|
+
"DfXygSm4jCyNCybVYYK6DwvWqjKee8pbDmJGcLWNDXjh",
|
|
68
|
+
"ADaUMid9yfUytqMBgopwjb2DTLSokTSzL1zt6iGPaS49",
|
|
69
|
+
"3AVi9Tg9Uo68tJfuvoKvqKNWKkC5wPdSSdeBnizKZ6jT",
|
|
70
|
+
"DttWaMuVvTiduZRnguLF7jNxTgiMBZ1hyAumKUiL2KRL",
|
|
71
|
+
"96gYZGLnJYVFmbjzopPSU6QiEV5fGqZNyN9nmNhvrZU5",
|
|
72
|
+
"ADuUkR4vqLUMWXxW9gh6D6L8pMSawimctcNZ5pGwDcEt",
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
export function getRandomTipAccount(): PublicKey {
|
|
76
|
+
const idx = Math.floor(Math.random() * TIP_ACCOUNTS.length);
|
|
77
|
+
return new PublicKey(TIP_ACCOUNTS[idx]);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function buildJitoTipInstruction(payer: PublicKey, lamports: number): TransactionInstruction {
|
|
81
|
+
return SystemProgram.transfer({
|
|
82
|
+
fromPubkey: payer,
|
|
83
|
+
toPubkey: getRandomTipAccount(),
|
|
84
|
+
lamports,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Get a random treasury ID (0-255) - same as Pyth SDK's getRandomTreasuryId()
|
|
90
|
+
*/
|
|
91
|
+
function getRandomTreasuryId(): number {
|
|
92
|
+
return Math.floor(Math.random() * 256);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Derives the Pyth price feed account address
|
|
97
|
+
*/
|
|
98
|
+
export function getPythPriceFeedAccountAddress(
|
|
99
|
+
shardId: number,
|
|
100
|
+
priceFeedId: string | Buffer,
|
|
101
|
+
pushOracleProgramId: PublicKey = DEFAULT_PUSH_ORACLE_PROGRAM_ID
|
|
102
|
+
): PublicKey {
|
|
103
|
+
let feedIdBuffer: Buffer;
|
|
104
|
+
if (typeof priceFeedId === "string") {
|
|
105
|
+
const hexString = priceFeedId.startsWith("0x")
|
|
106
|
+
? priceFeedId.slice(2)
|
|
107
|
+
: priceFeedId;
|
|
108
|
+
feedIdBuffer = Buffer.from(hexString, "hex");
|
|
109
|
+
} else {
|
|
110
|
+
feedIdBuffer = priceFeedId;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (feedIdBuffer.length !== 32) {
|
|
114
|
+
throw new Error("Feed ID should be 32 bytes long");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const shardBuffer = Buffer.alloc(2);
|
|
118
|
+
shardBuffer.writeUint16LE(shardId, 0);
|
|
119
|
+
|
|
120
|
+
return PublicKey.findProgramAddressSync(
|
|
121
|
+
[shardBuffer, feedIdBuffer],
|
|
122
|
+
pushOracleProgramId
|
|
123
|
+
)[0];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Get Treasury PDA for Pyth Receiver
|
|
128
|
+
*/
|
|
129
|
+
function getTreasuryPda(treasuryId: number, programId: PublicKey = DEFAULT_RECEIVER_PROGRAM_ID): PublicKey {
|
|
130
|
+
return PublicKey.findProgramAddressSync(
|
|
131
|
+
[Buffer.from("treasury"), Buffer.from([treasuryId])],
|
|
132
|
+
programId
|
|
133
|
+
)[0];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Get Config PDA for Pyth Receiver
|
|
138
|
+
*/
|
|
139
|
+
function getConfigPda(programId: PublicKey = DEFAULT_RECEIVER_PROGRAM_ID): PublicKey {
|
|
140
|
+
return PublicKey.findProgramAddressSync(
|
|
141
|
+
[Buffer.from("config")],
|
|
142
|
+
programId
|
|
143
|
+
)[0];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Get Guardian Set PDA for Wormhole
|
|
148
|
+
*/
|
|
149
|
+
function getGuardianSetPda(guardianSetIndex: number, programId: PublicKey = DEFAULT_WORMHOLE_PROGRAM_ID): PublicKey {
|
|
150
|
+
const indexBuffer = Buffer.alloc(4);
|
|
151
|
+
indexBuffer.writeUInt32BE(guardianSetIndex, 0);
|
|
152
|
+
return PublicKey.findProgramAddressSync(
|
|
153
|
+
[Buffer.from("GuardianSet"), indexBuffer],
|
|
154
|
+
programId
|
|
155
|
+
)[0];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Get guardian set index from VAA (offset 1, big-endian u32)
|
|
160
|
+
*/
|
|
161
|
+
function getGuardianSetIndex(vaa: Buffer): number {
|
|
162
|
+
return vaa.readUInt32BE(1);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Serialize Borsh Vec<u8> (4-byte LE length prefix + data)
|
|
168
|
+
*/
|
|
169
|
+
function serializeBorshBytes(data: Buffer): Buffer {
|
|
170
|
+
const lenBuffer = Buffer.alloc(4);
|
|
171
|
+
lenBuffer.writeUInt32LE(data.length, 0);
|
|
172
|
+
return Buffer.concat([lenBuffer, data]);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Serialize Borsh Vec<[u8; 20]> for merkle proof
|
|
177
|
+
*/
|
|
178
|
+
function serializeBorshProof(proof: Buffer[]): Buffer {
|
|
179
|
+
const lenBuffer = Buffer.alloc(4);
|
|
180
|
+
lenBuffer.writeUInt32LE(proof.length, 0);
|
|
181
|
+
return Buffer.concat([lenBuffer, ...proof]);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export interface InstructionWithSigners {
|
|
185
|
+
instruction: TransactionInstruction;
|
|
186
|
+
signers: Keypair[];
|
|
187
|
+
computeUnits?: number;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Build initEncodedVaa instruction
|
|
192
|
+
*/
|
|
193
|
+
function buildInitEncodedVaaInstruction(
|
|
194
|
+
writeAuthority: PublicKey,
|
|
195
|
+
encodedVaa: PublicKey,
|
|
196
|
+
wormholeProgramId: PublicKey = DEFAULT_WORMHOLE_PROGRAM_ID
|
|
197
|
+
): TransactionInstruction {
|
|
198
|
+
return new TransactionInstruction({
|
|
199
|
+
keys: [
|
|
200
|
+
{ pubkey: writeAuthority, isSigner: true, isWritable: false },
|
|
201
|
+
{ pubkey: encodedVaa, isSigner: false, isWritable: true },
|
|
202
|
+
],
|
|
203
|
+
programId: wormholeProgramId,
|
|
204
|
+
data: DISCRIMINATORS.initEncodedVaa,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Build writeEncodedVaa instruction
|
|
212
|
+
*/
|
|
213
|
+
function buildWriteEncodedVaaInstruction(
|
|
214
|
+
writeAuthority: PublicKey,
|
|
215
|
+
draftVaa: PublicKey,
|
|
216
|
+
index: number,
|
|
217
|
+
data: Buffer,
|
|
218
|
+
wormholeProgramId: PublicKey = DEFAULT_WORMHOLE_PROGRAM_ID
|
|
219
|
+
): TransactionInstruction {
|
|
220
|
+
// Serialize args: index (u32 LE) + data (Borsh Vec<u8>)
|
|
221
|
+
const indexBuffer = Buffer.alloc(4);
|
|
222
|
+
indexBuffer.writeUInt32LE(index, 0);
|
|
223
|
+
|
|
224
|
+
const instructionData = Buffer.concat([
|
|
225
|
+
DISCRIMINATORS.writeEncodedVaa,
|
|
226
|
+
indexBuffer,
|
|
227
|
+
serializeBorshBytes(data),
|
|
228
|
+
]);
|
|
229
|
+
|
|
230
|
+
return new TransactionInstruction({
|
|
231
|
+
keys: [
|
|
232
|
+
{ pubkey: writeAuthority, isSigner: true, isWritable: false },
|
|
233
|
+
{ pubkey: draftVaa, isSigner: false, isWritable: true },
|
|
234
|
+
],
|
|
235
|
+
programId: wormholeProgramId,
|
|
236
|
+
data: instructionData,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Build verifyEncodedVaaV1 instruction
|
|
242
|
+
*/
|
|
243
|
+
function buildVerifyEncodedVaaV1Instruction(
|
|
244
|
+
writeAuthority: PublicKey,
|
|
245
|
+
draftVaa: PublicKey,
|
|
246
|
+
guardianSetIndex: number,
|
|
247
|
+
wormholeProgramId: PublicKey = DEFAULT_WORMHOLE_PROGRAM_ID
|
|
248
|
+
): TransactionInstruction {
|
|
249
|
+
return new TransactionInstruction({
|
|
250
|
+
keys: [
|
|
251
|
+
{ pubkey: writeAuthority, isSigner: true, isWritable: false },
|
|
252
|
+
{ pubkey: draftVaa, isSigner: false, isWritable: true },
|
|
253
|
+
{ pubkey: getGuardianSetPda(guardianSetIndex, wormholeProgramId), isSigner: false, isWritable: false },
|
|
254
|
+
],
|
|
255
|
+
programId: wormholeProgramId,
|
|
256
|
+
data: DISCRIMINATORS.verifyEncodedVaaV1,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Build closeEncodedVaa instruction
|
|
262
|
+
*/
|
|
263
|
+
function buildCloseEncodedVaaInstruction(
|
|
264
|
+
writeAuthority: PublicKey,
|
|
265
|
+
encodedVaa: PublicKey,
|
|
266
|
+
wormholeProgramId: PublicKey = DEFAULT_WORMHOLE_PROGRAM_ID
|
|
267
|
+
): TransactionInstruction {
|
|
268
|
+
return new TransactionInstruction({
|
|
269
|
+
keys: [
|
|
270
|
+
{ pubkey: writeAuthority, isSigner: true, isWritable: true },
|
|
271
|
+
{ pubkey: encodedVaa, isSigner: false, isWritable: true },
|
|
272
|
+
],
|
|
273
|
+
programId: wormholeProgramId,
|
|
274
|
+
data: DISCRIMINATORS.closeEncodedVaa,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Build updatePriceFeed instruction for Pyth Push Oracle
|
|
280
|
+
*/
|
|
281
|
+
function buildUpdatePriceFeedInstruction(params: {
|
|
282
|
+
payer: PublicKey;
|
|
283
|
+
encodedVaa: PublicKey;
|
|
284
|
+
priceFeedAccount: PublicKey;
|
|
285
|
+
treasuryId: number;
|
|
286
|
+
shardId: number;
|
|
287
|
+
feedId: Buffer;
|
|
288
|
+
merklePriceUpdate: { message: Buffer; proof: Buffer[] };
|
|
289
|
+
receiverProgramId?: PublicKey;
|
|
290
|
+
pushOracleProgramId?: PublicKey;
|
|
291
|
+
}): TransactionInstruction {
|
|
292
|
+
const {
|
|
293
|
+
payer,
|
|
294
|
+
encodedVaa,
|
|
295
|
+
priceFeedAccount,
|
|
296
|
+
treasuryId,
|
|
297
|
+
shardId,
|
|
298
|
+
feedId,
|
|
299
|
+
merklePriceUpdate,
|
|
300
|
+
receiverProgramId = DEFAULT_RECEIVER_PROGRAM_ID,
|
|
301
|
+
pushOracleProgramId = DEFAULT_PUSH_ORACLE_PROGRAM_ID,
|
|
302
|
+
} = params;
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
const shardBuffer = Buffer.alloc(2);
|
|
306
|
+
shardBuffer.writeUint16LE(shardId, 0);
|
|
307
|
+
|
|
308
|
+
const data = Buffer.concat([
|
|
309
|
+
DISCRIMINATORS.updatePriceFeed,
|
|
310
|
+
// MerklePriceUpdate
|
|
311
|
+
serializeBorshBytes(merklePriceUpdate.message),
|
|
312
|
+
serializeBorshProof(merklePriceUpdate.proof),
|
|
313
|
+
// treasuryId (u8)
|
|
314
|
+
Buffer.from([treasuryId]),
|
|
315
|
+
// shardId (u16 LE)
|
|
316
|
+
shardBuffer,
|
|
317
|
+
// feedId ([u8; 32])
|
|
318
|
+
feedId,
|
|
319
|
+
]);
|
|
320
|
+
|
|
321
|
+
const keys = [
|
|
322
|
+
{ pubkey: payer, isSigner: true, isWritable: true },
|
|
323
|
+
{ pubkey: receiverProgramId, isSigner: false, isWritable: false },
|
|
324
|
+
{ pubkey: encodedVaa, isSigner: false, isWritable: false },
|
|
325
|
+
{ pubkey: getConfigPda(receiverProgramId), isSigner: false, isWritable: false },
|
|
326
|
+
{ pubkey: getTreasuryPda(treasuryId, receiverProgramId), isSigner: false, isWritable: true },
|
|
327
|
+
{ pubkey: priceFeedAccount, isSigner: false, isWritable: true },
|
|
328
|
+
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
|
|
329
|
+
];
|
|
330
|
+
|
|
331
|
+
return new TransactionInstruction({
|
|
332
|
+
keys,
|
|
333
|
+
programId: pushOracleProgramId,
|
|
334
|
+
data,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
export interface PythUpdateResult {
|
|
340
|
+
postInstructions: InstructionWithSigners[];
|
|
341
|
+
closeInstructions: InstructionWithSigners[];
|
|
342
|
+
signers: Keypair[];
|
|
343
|
+
priceFeedAccounts: Map<string, PublicKey>;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Convert proof from number[][] to Buffer[]
|
|
349
|
+
*/
|
|
350
|
+
function convertProof(proof: number[][]): Buffer[] {
|
|
351
|
+
return proof.map(p => Buffer.from(p));
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export function parsePriceFeedMessage(message: Buffer) {
|
|
355
|
+
let cursor = 0;
|
|
356
|
+
const variant = message.readUInt8(cursor); cursor += 1;
|
|
357
|
+
if (variant !== 0) throw new Error('Not a price feed message');
|
|
358
|
+
const feedId = message.subarray(cursor, cursor + 32); cursor += 32;
|
|
359
|
+
const price = message.subarray(cursor, cursor + 8); cursor += 8;
|
|
360
|
+
const confidence = message.subarray(cursor, cursor + 8); cursor += 8;
|
|
361
|
+
const exponent = message.readInt32BE(cursor); cursor += 4;
|
|
362
|
+
const publishTime = message.subarray(cursor, cursor + 8); cursor += 8;
|
|
363
|
+
const prevPublishTime = message.subarray(cursor, cursor + 8); cursor += 8;
|
|
364
|
+
const emaPrice = message.subarray(cursor, cursor + 8); cursor += 8;
|
|
365
|
+
const emaConf = message.subarray(cursor, cursor + 8);
|
|
366
|
+
return { feedId, price, confidence, exponent, publishTime, prevPublishTime, emaPrice, emaConf };
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** parsing of accumulator update data (VAA + updates) */
|
|
370
|
+
export function parseAccumulatorUpdateData(data: Buffer): { vaa: Buffer; updates: { message: Buffer; proof: number[][] }[] } {
|
|
371
|
+
const ACC_MAGIC = '504e4155';
|
|
372
|
+
if (data.toString('hex').slice(0, 8) !== ACC_MAGIC || data[4] !== 1 || data[5] !== 0) {
|
|
373
|
+
throw new Error('Invalid accumulator message');
|
|
374
|
+
}
|
|
375
|
+
let cursor = 6;
|
|
376
|
+
const trailingPayloadSize = data.readUInt8(cursor); cursor += 1 + trailingPayloadSize;
|
|
377
|
+
cursor += 1;
|
|
378
|
+
const vaaSize = data.readUInt16BE(cursor); cursor += 2;
|
|
379
|
+
const vaa = data.subarray(cursor, cursor + vaaSize); cursor += vaaSize;
|
|
380
|
+
const numUpdates = data.readUInt8(cursor); cursor += 1;
|
|
381
|
+
const updates: { message: Buffer; proof: number[][] }[] = [];
|
|
382
|
+
const HASH_SIZE = 20;
|
|
383
|
+
for (let i = 0; i < numUpdates; i++) {
|
|
384
|
+
const messageSize = data.readUInt16BE(cursor); cursor += 2;
|
|
385
|
+
const message = data.subarray(cursor, cursor + messageSize); cursor += messageSize;
|
|
386
|
+
const numProofs = data.readUInt8(cursor); cursor += 1;
|
|
387
|
+
const proof: number[][] = [];
|
|
388
|
+
for (let j = 0; j < numProofs; j++) {
|
|
389
|
+
proof.push(Array.from(data.subarray(cursor, cursor + HASH_SIZE)));
|
|
390
|
+
cursor += HASH_SIZE;
|
|
391
|
+
}
|
|
392
|
+
updates.push({ message, proof });
|
|
393
|
+
}
|
|
394
|
+
if (cursor !== data.length) throw new Error("Didn't reach end of message");
|
|
395
|
+
return { vaa, updates };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Get the size of a transaction that would contain the provided array of instructions
|
|
402
|
+
*/
|
|
403
|
+
export function getSizeOfTransaction(
|
|
404
|
+
instructions: TransactionInstruction[],
|
|
405
|
+
versionedTransaction: boolean = true,
|
|
406
|
+
addressLookupTable?: { state: { addresses: PublicKey[] } }
|
|
407
|
+
): number {
|
|
408
|
+
const programs = new Set<string>();
|
|
409
|
+
const signers = new Set<string>();
|
|
410
|
+
let accounts = new Set<string>();
|
|
411
|
+
instructions.map((ix) => {
|
|
412
|
+
programs.add(ix.programId.toBase58());
|
|
413
|
+
accounts.add(ix.programId.toBase58());
|
|
414
|
+
ix.keys.map((key) => {
|
|
415
|
+
if (key.isSigner) {
|
|
416
|
+
signers.add(key.pubkey.toBase58());
|
|
417
|
+
}
|
|
418
|
+
accounts.add(key.pubkey.toBase58());
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
const instruction_sizes = instructions
|
|
422
|
+
.map(
|
|
423
|
+
(ix) =>
|
|
424
|
+
1 +
|
|
425
|
+
getSizeOfCompressedU16(ix.keys.length) +
|
|
426
|
+
ix.keys.length +
|
|
427
|
+
getSizeOfCompressedU16(ix.data.length) +
|
|
428
|
+
ix.data.length
|
|
429
|
+
)
|
|
430
|
+
.reduce((a, b) => a + b, 0);
|
|
431
|
+
let numberOfAddressLookups = 0;
|
|
432
|
+
if (addressLookupTable) {
|
|
433
|
+
const lookupTableAddresses = addressLookupTable.state.addresses.map((address) => address.toBase58());
|
|
434
|
+
const totalNumberOfAccounts = accounts.size;
|
|
435
|
+
accounts = new Set([...accounts].filter((account) => !lookupTableAddresses.includes(account)));
|
|
436
|
+
accounts = new Set([...accounts, ...programs, ...signers]);
|
|
437
|
+
numberOfAddressLookups = totalNumberOfAccounts - accounts.size;
|
|
438
|
+
}
|
|
439
|
+
return (
|
|
440
|
+
getSizeOfCompressedU16(signers.size) +
|
|
441
|
+
signers.size * 64 +
|
|
442
|
+
3 +
|
|
443
|
+
getSizeOfCompressedU16(accounts.size) +
|
|
444
|
+
32 * accounts.size +
|
|
445
|
+
32 +
|
|
446
|
+
getSizeOfCompressedU16(instructions.length) +
|
|
447
|
+
instruction_sizes +
|
|
448
|
+
(versionedTransaction ? 1 + getSizeOfCompressedU16(0) : 0) +
|
|
449
|
+
(versionedTransaction && addressLookupTable ? 32 : 0) +
|
|
450
|
+
(versionedTransaction && addressLookupTable ? 2 : 0) +
|
|
451
|
+
numberOfAddressLookups
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/** Get the size of n in bytes when serialized as a CompressedU16 */
|
|
456
|
+
export function getSizeOfCompressedU16(n: number): number {
|
|
457
|
+
return 1 + Number(n >= 128) + Number(n >= 16384);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
const ENCODED_VAA_RENT_EXEMPTION = 7_836_960;
|
|
465
|
+
|
|
466
|
+
export interface InstructionBatch {
|
|
467
|
+
instructions: TransactionInstruction[];
|
|
468
|
+
signers: Keypair[];
|
|
469
|
+
computeUnits: number;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Fetch Pyth price accounts from RPC and extract feed IDs
|
|
474
|
+
*/
|
|
475
|
+
export async function fetchFeedIdsFromAccounts(
|
|
476
|
+
connection: Connection,
|
|
477
|
+
priceAccounts: PublicKey[],
|
|
478
|
+
): Promise<{ feedIds: string[], feedIdToAccount: Map<string, PublicKey> }> {
|
|
479
|
+
const accountInfos = await connection.getMultipleAccountsInfo(priceAccounts, "confirmed");
|
|
480
|
+
const feedIds: string[] = [];
|
|
481
|
+
const feedIdToAccount = new Map<string, PublicKey>();
|
|
482
|
+
|
|
483
|
+
for (let i = 0; i < priceAccounts.length; i++) {
|
|
484
|
+
const ai = accountInfos[i];
|
|
485
|
+
if (!ai) throw new Error(`Account ${priceAccounts[i].toBase58()} not found`);
|
|
486
|
+
const [state, _] = PythState.decode(ai.data, 8);
|
|
487
|
+
const feedIdHex = "0x" + state.priceMessage.feedId.toString("hex");
|
|
488
|
+
feedIds.push(feedIdHex);
|
|
489
|
+
feedIdToAccount.set(feedIdHex, priceAccounts[i]);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
return { feedIds, feedIdToAccount };
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
export interface PythUpdateTxResult {
|
|
497
|
+
txBatchData: TxBatchData;
|
|
498
|
+
/** Keypairs that must sign their respective transactions before sending */
|
|
499
|
+
extraSigners: Map<number, Keypair[]>; // batchIndex -> signers for tx[0] in that batch
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Build Pyth price feed update as TxBatchData compatible with SDK txUtils.
|
|
504
|
+
*
|
|
505
|
+
* TxBatchData.batches layout:
|
|
506
|
+
* batch 0 (sequential): [createAccount + initEncodedVaa + writeVaa_part1]
|
|
507
|
+
* batch 1 (sequential): [writeVaa_part2 + verifyEncodedVaa]
|
|
508
|
+
* batch 2 (parallel): [updateFeed_0], [updateFeed_1], ... [updateFeed_N-2]
|
|
509
|
+
* batch 3 (sequential): [updateFeed_last + closeEncodedVaa]
|
|
510
|
+
*
|
|
511
|
+
* Transactions within each batch are sent in parallel by sendVersionedTxs.
|
|
512
|
+
* Batches are sent sequentially.
|
|
513
|
+
*/
|
|
514
|
+
export async function buildPythPriceFeedUpdateIxs(
|
|
515
|
+
payer: PublicKey,
|
|
516
|
+
priceFeedIds: string[],
|
|
517
|
+
defaultShardId?: number,
|
|
518
|
+
hermesClient?: HermesClient,
|
|
519
|
+
): Promise<{
|
|
520
|
+
vaaCreateInitEncodeIxs: { ixs: TransactionInstruction[]; signer: Keypair; }[];
|
|
521
|
+
vaaWriteVerifyIxs: TransactionInstruction[][];
|
|
522
|
+
updateFeedIxs: TransactionInstruction[];
|
|
523
|
+
closeVaaIxs: TransactionInstruction[];
|
|
524
|
+
}> {
|
|
525
|
+
const client = hermesClient ?? new HermesClient(HERMES_PUBLIC_ENDPOINT, {});
|
|
526
|
+
const shardId = defaultShardId ?? 0;
|
|
527
|
+
const priceUpdates = await client.getLatestPriceUpdates(priceFeedIds, { encoding: "base64" });
|
|
528
|
+
|
|
529
|
+
if (!priceUpdates.binary?.data || priceUpdates.binary.data.length === 0) {
|
|
530
|
+
throw new Error("Failed to fetch price updates from Hermes");
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const treasuryId = getRandomTreasuryId();
|
|
534
|
+
|
|
535
|
+
let allVaaCreateInitEncodeIxs: { ixs: TransactionInstruction[]; signer: Keypair; }[] = [];
|
|
536
|
+
let allVaaWriteVerifyIxs: TransactionInstruction[][] = [];
|
|
537
|
+
const allUpdateIxs: TransactionInstruction[] = [];
|
|
538
|
+
const allCloseIxs: TransactionInstruction[] = [];
|
|
539
|
+
|
|
540
|
+
for (const updateData of priceUpdates.binary.data) {
|
|
541
|
+
const accumulatorData = parseAccumulatorUpdateData(Buffer.from(updateData, "base64"));
|
|
542
|
+
const vaa = accumulatorData.vaa;
|
|
543
|
+
const encodedVaaKeypair = Keypair.generate();
|
|
544
|
+
const encodedVaaSize = vaa.length + VAA_START;
|
|
545
|
+
const guardianSetIndex = getGuardianSetIndex(vaa);
|
|
546
|
+
|
|
547
|
+
let vaaCreateInitEncodeIxs: TransactionInstruction[] = [];
|
|
548
|
+
let vaaWriteVerifyIxs: TransactionInstruction[] = [];
|
|
549
|
+
|
|
550
|
+
// createAccount
|
|
551
|
+
vaaCreateInitEncodeIxs.push(
|
|
552
|
+
SystemProgram.createAccount({
|
|
553
|
+
fromPubkey: payer,
|
|
554
|
+
newAccountPubkey: encodedVaaKeypair.publicKey,
|
|
555
|
+
lamports: ENCODED_VAA_RENT_EXEMPTION,
|
|
556
|
+
space: encodedVaaSize,
|
|
557
|
+
programId: DEFAULT_WORMHOLE_PROGRAM_ID,
|
|
558
|
+
})
|
|
559
|
+
);
|
|
560
|
+
|
|
561
|
+
// initEncodedVaa
|
|
562
|
+
vaaCreateInitEncodeIxs.push(
|
|
563
|
+
buildInitEncodedVaaInstruction(payer, encodedVaaKeypair.publicKey)
|
|
564
|
+
);
|
|
565
|
+
|
|
566
|
+
// writeEncodedVaa part 1
|
|
567
|
+
const firstPartEnd = Math.min(VAA_SPLIT_INDEX, vaa.length);
|
|
568
|
+
vaaCreateInitEncodeIxs.push(
|
|
569
|
+
buildWriteEncodedVaaInstruction(payer, encodedVaaKeypair.publicKey, 0, vaa.subarray(0, firstPartEnd))
|
|
570
|
+
);
|
|
571
|
+
|
|
572
|
+
// writeEncodedVaa part 2 (if needed)
|
|
573
|
+
if (vaa.length > VAA_SPLIT_INDEX) {
|
|
574
|
+
vaaWriteVerifyIxs.push(
|
|
575
|
+
buildWriteEncodedVaaInstruction(payer, encodedVaaKeypair.publicKey, VAA_SPLIT_INDEX, vaa.subarray(VAA_SPLIT_INDEX))
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// verifyEncodedVaaV1
|
|
580
|
+
vaaWriteVerifyIxs.push(
|
|
581
|
+
buildVerifyEncodedVaaV1Instruction(payer, encodedVaaKeypair.publicKey, guardianSetIndex)
|
|
582
|
+
);
|
|
583
|
+
|
|
584
|
+
// updateFeed per price update
|
|
585
|
+
for (const update of accumulatorData.updates) {
|
|
586
|
+
const parsedMessage = parsePriceFeedMessage(update.message);
|
|
587
|
+
const feedId = parsedMessage.feedId;
|
|
588
|
+
|
|
589
|
+
allUpdateIxs.push(
|
|
590
|
+
buildUpdatePriceFeedInstruction({
|
|
591
|
+
payer,
|
|
592
|
+
encodedVaa: encodedVaaKeypair.publicKey,
|
|
593
|
+
priceFeedAccount: getPythPriceFeedAccountAddress(shardId, feedId),
|
|
594
|
+
treasuryId,
|
|
595
|
+
shardId,
|
|
596
|
+
feedId,
|
|
597
|
+
merklePriceUpdate: {
|
|
598
|
+
message: update.message,
|
|
599
|
+
proof: convertProof(update.proof),
|
|
600
|
+
},
|
|
601
|
+
}),
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
allVaaCreateInitEncodeIxs.push({
|
|
606
|
+
ixs: vaaCreateInitEncodeIxs,
|
|
607
|
+
signer: encodedVaaKeypair,
|
|
608
|
+
});
|
|
609
|
+
allVaaWriteVerifyIxs.push(vaaWriteVerifyIxs);
|
|
610
|
+
// closeEncodedVaa
|
|
611
|
+
allCloseIxs.push(
|
|
612
|
+
buildCloseEncodedVaaInstruction(payer, encodedVaaKeypair.publicKey),
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
return {
|
|
617
|
+
vaaCreateInitEncodeIxs: allVaaCreateInitEncodeIxs,
|
|
618
|
+
vaaWriteVerifyIxs: allVaaWriteVerifyIxs,
|
|
619
|
+
updateFeedIxs: allUpdateIxs,
|
|
620
|
+
closeVaaIxs: allCloseIxs,
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
export class PriceFeedMessage {
|
|
626
|
+
feedId: Buffer;
|
|
627
|
+
price: BN; // i64 (signed)
|
|
628
|
+
conf: BN; // u64
|
|
629
|
+
exponent: number; // i32
|
|
630
|
+
publishTime: BN; // i64
|
|
631
|
+
prevPublishTime: BN; // i64
|
|
632
|
+
emaPrice: BN; // i64
|
|
633
|
+
emaConf: BN; // u64
|
|
634
|
+
|
|
635
|
+
constructor(params: {
|
|
636
|
+
feedId: Buffer,
|
|
637
|
+
price: BN,
|
|
638
|
+
conf: BN,
|
|
639
|
+
exponent: number,
|
|
640
|
+
publishTime: BN,
|
|
641
|
+
prevPublishTime: BN,
|
|
642
|
+
emaPrice: BN,
|
|
643
|
+
emaConf: BN
|
|
644
|
+
}) {
|
|
645
|
+
this.feedId = params.feedId;
|
|
646
|
+
this.price = params.price;
|
|
647
|
+
this.conf = params.conf;
|
|
648
|
+
this.exponent = params.exponent;
|
|
649
|
+
this.publishTime = params.publishTime;
|
|
650
|
+
this.prevPublishTime = params.prevPublishTime;
|
|
651
|
+
this.emaPrice = params.emaPrice;
|
|
652
|
+
this.emaConf = params.emaConf;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
static decode(buf: Buffer, offset: number = 0): [PriceFeedMessage, number] {
|
|
656
|
+
let cursor = offset;
|
|
657
|
+
|
|
658
|
+
// feed_id: [u8;32]
|
|
659
|
+
const feedId = buf.subarray(cursor, cursor + 32);
|
|
660
|
+
cursor += 32;
|
|
661
|
+
|
|
662
|
+
// price: i64 (signed)
|
|
663
|
+
const price = new BN(buf.subarray(cursor, cursor + 8), "le").fromTwos(64);
|
|
664
|
+
cursor += 8;
|
|
665
|
+
|
|
666
|
+
// conf: u64 (unsigned)
|
|
667
|
+
const conf = new BN(buf.subarray(cursor, cursor + 8), "le");
|
|
668
|
+
cursor += 8;
|
|
669
|
+
|
|
670
|
+
// exponent: i32 (signed)
|
|
671
|
+
const exponent = new BN(buf.subarray(cursor, cursor + 4), "le").fromTwos(32).toNumber();
|
|
672
|
+
cursor += 4;
|
|
673
|
+
|
|
674
|
+
// publish_time: i64 (signed)
|
|
675
|
+
const publishTime = new BN(buf.subarray(cursor, cursor + 8), "le").fromTwos(64);
|
|
676
|
+
cursor += 8;
|
|
677
|
+
|
|
678
|
+
// prev_publish_time: i64 (signed)
|
|
679
|
+
const prevPublishTime = new BN(buf.subarray(cursor, cursor + 8), "le").fromTwos(64);
|
|
680
|
+
cursor += 8;
|
|
681
|
+
|
|
682
|
+
// ema_price: i64 (signed)
|
|
683
|
+
const emaPrice = new BN(buf.subarray(cursor, cursor + 8), "le").fromTwos(64);
|
|
684
|
+
cursor += 8;
|
|
685
|
+
|
|
686
|
+
// ema_conf: u64 (unsigned)
|
|
687
|
+
const emaConf = new BN(buf.subarray(cursor, cursor + 8), "le");
|
|
688
|
+
cursor += 8;
|
|
689
|
+
|
|
690
|
+
return [ new PriceFeedMessage({
|
|
691
|
+
feedId,
|
|
692
|
+
price,
|
|
693
|
+
conf,
|
|
694
|
+
exponent,
|
|
695
|
+
publishTime,
|
|
696
|
+
prevPublishTime,
|
|
697
|
+
emaPrice,
|
|
698
|
+
emaConf
|
|
699
|
+
}), cursor ];
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
export class PythState {
|
|
704
|
+
writeAuthority: PublicKey;
|
|
705
|
+
verificationLevel: VerificationLevel;
|
|
706
|
+
priceMessage: PriceFeedMessage;
|
|
707
|
+
postedSlot: BN;
|
|
708
|
+
|
|
709
|
+
constructor(
|
|
710
|
+
writeAuthority: PublicKey,
|
|
711
|
+
verificationLevel: VerificationLevel,
|
|
712
|
+
priceMessage: PriceFeedMessage,
|
|
713
|
+
postedSlot: BN
|
|
714
|
+
) {
|
|
715
|
+
this.writeAuthority = writeAuthority;
|
|
716
|
+
this.verificationLevel = verificationLevel;
|
|
717
|
+
this.priceMessage = priceMessage;
|
|
718
|
+
this.postedSlot = postedSlot;
|
|
719
|
+
};
|
|
720
|
+
|
|
721
|
+
static decode(buf: Buffer, offset: number = 0): [PythState, number] {
|
|
722
|
+
let cursor = offset;
|
|
723
|
+
// write_authority: Pubkey (32)
|
|
724
|
+
const writeAuthority = new PublicKey(buf.subarray(cursor, cursor + 32));
|
|
725
|
+
cursor += 32;
|
|
726
|
+
|
|
727
|
+
const { level, size } = parseVerificationLevel(buf, cursor);
|
|
728
|
+
cursor += size;
|
|
729
|
+
|
|
730
|
+
// price_message: PriceFeedMessage
|
|
731
|
+
const [priceMessage, next] = PriceFeedMessage.decode(buf, cursor);
|
|
732
|
+
cursor = next;
|
|
733
|
+
|
|
734
|
+
// posted_slot: u64
|
|
735
|
+
const postedSlot = new BN(buf.subarray(cursor, cursor + 8), "le");
|
|
736
|
+
cursor += 8;
|
|
737
|
+
|
|
738
|
+
return [ new PythState(writeAuthority, level, priceMessage, postedSlot), cursor ];
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
|
|
744
|
+
export class PythOracle {
|
|
745
|
+
|
|
746
|
+
static fetch(
|
|
747
|
+
oracleParams: OracleSettings,
|
|
748
|
+
accountInfos: AccountInfo<Buffer>[],
|
|
749
|
+
solPrice?: OraclePrice,
|
|
750
|
+
usdPrice?: OraclePrice,
|
|
751
|
+
): OraclePrice {
|
|
752
|
+
//@ts-ignore
|
|
753
|
+
let state: PythState = null;
|
|
754
|
+
try {
|
|
755
|
+
const stateAi = accountInfos[0];
|
|
756
|
+
const [parsedState, _] = PythState.decode(stateAi.data, 8);
|
|
757
|
+
state = parsedState;
|
|
758
|
+
} catch (error) {
|
|
759
|
+
return new OraclePrice(new Decimal(0), new Decimal(0), 0);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
let pr = new Decimal(state.priceMessage.price.toString()).mul(Decimal.pow(10, state.priceMessage.exponent - oracleParams.tokenDecimals));
|
|
763
|
+
let ema = new Decimal(state.priceMessage.emaPrice.toString()).mul(Decimal.pow(10, state.priceMessage.exponent - oracleParams.tokenDecimals));
|
|
764
|
+
let cf = new Decimal(state.priceMessage.conf.toString()).mul(Decimal.pow(10, state.priceMessage.exponent - oracleParams.tokenDecimals));
|
|
765
|
+
const lastUpdateTimestamp = state.priceMessage.publishTime;
|
|
766
|
+
|
|
767
|
+
// Validate primary price is not zero
|
|
768
|
+
if (pr.lte(0)) {
|
|
769
|
+
return new OraclePrice(new Decimal(0), new Decimal(0), 0);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// === 1. Inflate confidence by staleness ===
|
|
773
|
+
// confidence = confidence * (1 + delta_t * stalenessConfRateBps / 10_000)
|
|
774
|
+
const now: BN = new BN(Math.floor(Date.now() / 1000));
|
|
775
|
+
const deltaSeconds = Math.max(now.toNumber() - lastUpdateTimestamp.toNumber(), 0);
|
|
776
|
+
const deltaT = new Decimal(deltaSeconds);
|
|
777
|
+
const deltaTBN = new BN(deltaSeconds);
|
|
778
|
+
const stalenessRate = new Decimal(oracleParams.stalenessConfRateBps).div(new Decimal(HUNDRED_PERCENT_BPS));
|
|
779
|
+
const inflateFactor = new Decimal(1).add(deltaT.mul(stalenessRate));
|
|
780
|
+
cf = cf.mul(inflateFactor);
|
|
781
|
+
|
|
782
|
+
let validated: boolean = true;
|
|
783
|
+
// === 2. Validate confidence threshold ===
|
|
784
|
+
// confidence / price * 10_000 < confThreshBps
|
|
785
|
+
const confRatioBps = cf.div(pr).mul(new Decimal(HUNDRED_PERCENT_BPS));
|
|
786
|
+
if (confRatioBps.gt(new Decimal(oracleParams.confThreshBps)))
|
|
787
|
+
validated = false;
|
|
788
|
+
|
|
789
|
+
// === 3. Validate staleness threshold ===
|
|
790
|
+
if (deltaTBN.gt(oracleParams.stalenessThresh))
|
|
791
|
+
validated = false;
|
|
792
|
+
|
|
793
|
+
// === 4. Validate volatility threshold using EMA and price ===
|
|
794
|
+
// max(ema, price) / min(ema, price) * 10_000 < volatilityThreshBps
|
|
795
|
+
const maxPrice = Decimal.max(ema, pr);
|
|
796
|
+
const minPrice = Decimal.min(ema, pr);
|
|
797
|
+
if (!minPrice.eq(0)) {
|
|
798
|
+
const volRatio = maxPrice.sub(minPrice).div(minPrice);
|
|
799
|
+
const volRatioBps = volRatio.mul(new Decimal(HUNDRED_PERCENT_BPS));
|
|
800
|
+
if (volRatioBps.gt(new Decimal(oracleParams.volatilityThreshBps))) {
|
|
801
|
+
validated = false;
|
|
802
|
+
}
|
|
803
|
+
} else {
|
|
804
|
+
validated = false;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// we don't validate liquidity for Pyth
|
|
808
|
+
|
|
809
|
+
return new OraclePrice(
|
|
810
|
+
pr,
|
|
811
|
+
cf,
|
|
812
|
+
parseInt(state.priceMessage.publishTime.toString()),
|
|
813
|
+
validated
|
|
814
|
+
);
|
|
815
|
+
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
|
|
819
|
+
|
|
820
|
+
// async fetchPriceFromHermes(priceFeedId: string): Promise<PythState> {
|
|
821
|
+
// const priceUpdate = await this.priceClient.getLatestPriceUpdates([priceFeedId], {encoding: "base64"});
|
|
822
|
+
// if(priceUpdate && priceUpdate.parsed && priceUpdate.parsed.length > 0) {
|
|
823
|
+
// const priceData = priceUpdate.parsed[0].price;
|
|
824
|
+
// return {
|
|
825
|
+
// price: new BN(priceData.price),
|
|
826
|
+
// conf: new BN(priceData.conf),
|
|
827
|
+
// expo: priceData.expo,
|
|
828
|
+
// publishTime: new BN(priceData.publishTime)
|
|
829
|
+
// };
|
|
830
|
+
// } else {
|
|
831
|
+
// throw new Error(`Failed to fetch price update from Hermes for ${priceFeedId}`);
|
|
832
|
+
// }
|
|
833
|
+
// }
|
|
834
|
+
|
|
835
|
+
// /**
|
|
836
|
+
// * Should set feedId before running this
|
|
837
|
+
// * @param {number} shardId - between 0-2^64, default is 0
|
|
838
|
+
// * @param {number} [computeUnitPrice] - compute unit price in microlamports, default is 100_000
|
|
839
|
+
// */
|
|
840
|
+
// async updateFeedTx(wallet: any, shardId: number = 0, computeUnitPrice: number = 100000) {
|
|
841
|
+
// if(!shardId) shardId = 0;
|
|
842
|
+
// if(!computeUnitPrice) computeUnitPrice = 100000;
|
|
843
|
+
// let priceUpdate = await this.priceClient.getLatestPriceUpdates([this.priceFeedId], {encoding: "base64"});
|
|
844
|
+
// const pythSolanaReceiver = new PythSolanaReceiver({
|
|
845
|
+
// connection: this.connection,
|
|
846
|
+
// wallet: wallet,
|
|
847
|
+
// });
|
|
848
|
+
// const transactionBuilder = pythSolanaReceiver.newTransactionBuilder({
|
|
849
|
+
// // closeUpdateAccounts: false
|
|
850
|
+
// });
|
|
851
|
+
// await transactionBuilder.addUpdatePriceFeed(priceUpdate.binary.data, shardId);
|
|
852
|
+
// let build = await transactionBuilder.buildVersionedTransactions({
|
|
853
|
+
// computeUnitPriceMicroLamports: computeUnitPrice,
|
|
854
|
+
// });
|
|
855
|
+
|
|
856
|
+
// return build;
|
|
857
|
+
// };
|
|
858
|
+
|
|
859
|
+
// async sendUpdateTx(
|
|
860
|
+
// provider: AnchorProvider,
|
|
861
|
+
// updateFeedTx: {
|
|
862
|
+
// tx: VersionedTransaction;
|
|
863
|
+
// signers: Signer[];
|
|
864
|
+
// }[]
|
|
865
|
+
// ): Promise<TransactionSignature[]>{
|
|
866
|
+
|
|
867
|
+
// let txs = await provider.sendAll(updateFeedTx, { skipPreflight: true, commitment: "confirmed"});
|
|
868
|
+
// return txs;
|
|
869
|
+
// }
|
|
870
|
+
|
|
871
|
+
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
|