@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.
Files changed (121) hide show
  1. package/dist/src/constants.d.ts +23 -0
  2. package/dist/src/constants.js +38 -0
  3. package/dist/src/index.d.ts +804 -0
  4. package/dist/src/index.js +2097 -0
  5. package/dist/src/instructions/automation/auction.d.ts +6 -0
  6. package/dist/src/instructions/automation/auction.js +40 -0
  7. package/dist/src/instructions/automation/claimBounty.d.ts +12 -0
  8. package/dist/src/instructions/automation/claimBounty.js +44 -0
  9. package/dist/src/instructions/automation/flashSwap.d.ts +21 -0
  10. package/dist/src/instructions/automation/flashSwap.js +74 -0
  11. package/dist/src/instructions/automation/priceUpdate.d.ts +19 -0
  12. package/dist/src/instructions/automation/priceUpdate.js +89 -0
  13. package/dist/src/instructions/automation/rebalanceIntent.d.ts +32 -0
  14. package/dist/src/instructions/automation/rebalanceIntent.js +117 -0
  15. package/dist/src/instructions/automation/rebalanceSwap.d.ts +11 -0
  16. package/dist/src/instructions/automation/rebalanceSwap.js +42 -0
  17. package/dist/src/instructions/management/addBounty.d.ts +7 -0
  18. package/dist/src/instructions/management/addBounty.js +41 -0
  19. package/dist/src/instructions/management/admin.d.ts +9 -0
  20. package/dist/src/instructions/management/admin.js +53 -0
  21. package/dist/src/instructions/management/claimFees.d.ts +15 -0
  22. package/dist/src/instructions/management/claimFees.js +95 -0
  23. package/dist/src/instructions/management/createBasket.d.ts +21 -0
  24. package/dist/src/instructions/management/createBasket.js +98 -0
  25. package/dist/src/instructions/management/edit.d.ts +51 -0
  26. package/dist/src/instructions/management/edit.js +477 -0
  27. package/dist/src/instructions/management/luts.d.ts +30 -0
  28. package/dist/src/instructions/management/luts.js +99 -0
  29. package/dist/src/instructions/pda.d.ts +25 -0
  30. package/dist/src/instructions/pda.js +128 -0
  31. package/dist/src/instructions/user/deposit.d.ts +20 -0
  32. package/dist/src/instructions/user/deposit.js +100 -0
  33. package/dist/src/instructions/user/withdraw.d.ts +8 -0
  34. package/dist/src/instructions/user/withdraw.js +36 -0
  35. package/dist/src/jup.d.ts +49 -0
  36. package/dist/src/jup.js +80 -0
  37. package/dist/src/keeperMonitor.d.ts +52 -0
  38. package/dist/src/keeperMonitor.js +624 -0
  39. package/dist/src/layouts/basket.d.ts +191 -0
  40. package/dist/src/layouts/basket.js +51 -0
  41. package/dist/src/layouts/config.d.ts +281 -0
  42. package/dist/src/layouts/config.js +237 -0
  43. package/dist/src/layouts/fraction.d.ts +20 -0
  44. package/dist/src/layouts/fraction.js +164 -0
  45. package/dist/src/layouts/intents/bounty.d.ts +18 -0
  46. package/dist/src/layouts/intents/bounty.js +19 -0
  47. package/dist/src/layouts/intents/intent.d.ts +209 -0
  48. package/dist/src/layouts/intents/intent.js +97 -0
  49. package/dist/src/layouts/intents/rebalanceIntent.d.ts +212 -0
  50. package/dist/src/layouts/intents/rebalanceIntent.js +94 -0
  51. package/dist/src/layouts/lookupTable.d.ts +7 -0
  52. package/dist/src/layouts/lookupTable.js +10 -0
  53. package/dist/src/layouts/oracle.d.ts +63 -0
  54. package/dist/src/layouts/oracle.js +96 -0
  55. package/dist/src/states/basket.d.ts +14 -0
  56. package/dist/src/states/basket.js +479 -0
  57. package/dist/src/states/config.d.ts +3 -0
  58. package/dist/src/states/config.js +71 -0
  59. package/dist/src/states/intents/intent.d.ts +10 -0
  60. package/dist/src/states/intents/intent.js +316 -0
  61. package/dist/src/states/intents/rebalanceIntent.d.ts +42 -0
  62. package/dist/src/states/intents/rebalanceIntent.js +680 -0
  63. package/dist/src/states/oracles/constants.d.ts +9 -0
  64. package/dist/src/states/oracles/constants.js +15 -0
  65. package/dist/src/states/oracles/oracle.d.ts +24 -0
  66. package/dist/src/states/oracles/oracle.js +168 -0
  67. package/dist/src/states/oracles/pythOracle.d.ts +132 -0
  68. package/dist/src/states/oracles/pythOracle.js +609 -0
  69. package/dist/src/states/oracles/raydiumClmmOracle.d.ts +184 -0
  70. package/dist/src/states/oracles/raydiumClmmOracle.js +843 -0
  71. package/dist/src/states/oracles/raydiumCpmmOracle.d.ts +120 -0
  72. package/dist/src/states/oracles/raydiumCpmmOracle.js +540 -0
  73. package/dist/src/states/oracles/switchboardOracle.d.ts +0 -0
  74. package/dist/src/states/oracles/switchboardOracle.js +1 -0
  75. package/dist/src/states/withdrawBasketFees.d.ts +10 -0
  76. package/dist/src/states/withdrawBasketFees.js +154 -0
  77. package/dist/src/txUtils.d.ts +65 -0
  78. package/dist/src/txUtils.js +306 -0
  79. package/dist/test.d.ts +1 -0
  80. package/dist/test.js +561 -0
  81. package/package.json +31 -0
  82. package/src/constants.ts +40 -0
  83. package/src/index.ts +2431 -0
  84. package/src/instructions/automation/auction.ts +55 -0
  85. package/src/instructions/automation/claimBounty.ts +69 -0
  86. package/src/instructions/automation/flashSwap.ts +104 -0
  87. package/src/instructions/automation/priceUpdate.ts +117 -0
  88. package/src/instructions/automation/rebalanceIntent.ts +181 -0
  89. package/src/instructions/management/addBounty.ts +55 -0
  90. package/src/instructions/management/admin.ts +72 -0
  91. package/src/instructions/management/claimFees.ts +129 -0
  92. package/src/instructions/management/createBasket.ts +138 -0
  93. package/src/instructions/management/edit.ts +602 -0
  94. package/src/instructions/management/luts.ts +157 -0
  95. package/src/instructions/pda.ts +151 -0
  96. package/src/instructions/user/deposit.ts +143 -0
  97. package/src/instructions/user/withdraw.ts +53 -0
  98. package/src/jup.ts +113 -0
  99. package/src/keeperMonitor.ts +585 -0
  100. package/src/layouts/basket.ts +233 -0
  101. package/src/layouts/config.ts +576 -0
  102. package/src/layouts/fraction.ts +164 -0
  103. package/src/layouts/intents/bounty.ts +35 -0
  104. package/src/layouts/intents/intent.ts +324 -0
  105. package/src/layouts/intents/rebalanceIntent.ts +306 -0
  106. package/src/layouts/lookupTable.ts +14 -0
  107. package/src/layouts/oracle.ts +157 -0
  108. package/src/states/basket.ts +527 -0
  109. package/src/states/config.ts +62 -0
  110. package/src/states/intents/intent.ts +311 -0
  111. package/src/states/intents/rebalanceIntent.ts +751 -0
  112. package/src/states/oracles/constants.ts +13 -0
  113. package/src/states/oracles/oracle.ts +212 -0
  114. package/src/states/oracles/pythOracle.ts +874 -0
  115. package/src/states/oracles/raydiumClmmOracle.ts +1193 -0
  116. package/src/states/oracles/raydiumCpmmOracle.ts +784 -0
  117. package/src/states/oracles/switchboardOracle.ts +0 -0
  118. package/src/states/withdrawBasketFees.ts +160 -0
  119. package/src/txUtils.ts +424 -0
  120. package/test.ts +609 -0
  121. 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
+