@epicentral/sos-sdk 0.12.1-beta → 0.13.0-beta

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.
@@ -48,6 +48,7 @@ export type UpdateImpliedVolatilityInstruction<
48
48
  TAccountOptionAccount extends string | AccountMeta<string> = string,
49
49
  TAccountOptionPool extends string | AccountMeta<string> = string,
50
50
  TAccountMarketData extends string | AccountMeta<string> = string,
51
+ TAccountVolatilityQuoteAccount extends string | AccountMeta<string> = string,
51
52
  TAccountKeeper extends string | AccountMeta<string> = string,
52
53
  TRemainingAccounts extends readonly AccountMeta<string>[] = [],
53
54
  > = Instruction<TProgram> &
@@ -63,6 +64,9 @@ export type UpdateImpliedVolatilityInstruction<
63
64
  TAccountMarketData extends string
64
65
  ? ReadonlyAccount<TAccountMarketData>
65
66
  : TAccountMarketData,
67
+ TAccountVolatilityQuoteAccount extends string
68
+ ? ReadonlyAccount<TAccountVolatilityQuoteAccount>
69
+ : TAccountVolatilityQuoteAccount,
66
70
  TAccountKeeper extends string
67
71
  ? ReadonlySignerAccount<TAccountKeeper> &
68
72
  AccountSignerMeta<TAccountKeeper>
@@ -107,6 +111,7 @@ export type UpdateImpliedVolatilityInput<
107
111
  TAccountOptionAccount extends string = string,
108
112
  TAccountOptionPool extends string = string,
109
113
  TAccountMarketData extends string = string,
114
+ TAccountVolatilityQuoteAccount extends string = string,
110
115
  TAccountKeeper extends string = string,
111
116
  > = {
112
117
  /** The option account to update IV for */
@@ -115,6 +120,7 @@ export type UpdateImpliedVolatilityInput<
115
120
  optionPool: Address<TAccountOptionPool>;
116
121
  /** Market data - provides baseline historical volatility */
117
122
  marketData: Address<TAccountMarketData>;
123
+ volatilityQuoteAccount: Address<TAccountVolatilityQuoteAccount>;
118
124
  /** Keeper/anyone can call this instruction (permissionless update) */
119
125
  keeper: TransactionSigner<TAccountKeeper>;
120
126
  };
@@ -123,6 +129,7 @@ export function getUpdateImpliedVolatilityInstruction<
123
129
  TAccountOptionAccount extends string,
124
130
  TAccountOptionPool extends string,
125
131
  TAccountMarketData extends string,
132
+ TAccountVolatilityQuoteAccount extends string,
126
133
  TAccountKeeper extends string,
127
134
  TProgramAddress extends Address = typeof OPTION_PROGRAM_PROGRAM_ADDRESS,
128
135
  >(
@@ -130,6 +137,7 @@ export function getUpdateImpliedVolatilityInstruction<
130
137
  TAccountOptionAccount,
131
138
  TAccountOptionPool,
132
139
  TAccountMarketData,
140
+ TAccountVolatilityQuoteAccount,
133
141
  TAccountKeeper
134
142
  >,
135
143
  config?: { programAddress?: TProgramAddress },
@@ -138,6 +146,7 @@ export function getUpdateImpliedVolatilityInstruction<
138
146
  TAccountOptionAccount,
139
147
  TAccountOptionPool,
140
148
  TAccountMarketData,
149
+ TAccountVolatilityQuoteAccount,
141
150
  TAccountKeeper
142
151
  > {
143
152
  // Program address.
@@ -149,6 +158,10 @@ export function getUpdateImpliedVolatilityInstruction<
149
158
  optionAccount: { value: input.optionAccount ?? null, isWritable: true },
150
159
  optionPool: { value: input.optionPool ?? null, isWritable: false },
151
160
  marketData: { value: input.marketData ?? null, isWritable: false },
161
+ volatilityQuoteAccount: {
162
+ value: input.volatilityQuoteAccount ?? null,
163
+ isWritable: false,
164
+ },
152
165
  keeper: { value: input.keeper ?? null, isWritable: false },
153
166
  };
154
167
  const accounts = originalAccounts as Record<
@@ -162,6 +175,7 @@ export function getUpdateImpliedVolatilityInstruction<
162
175
  getAccountMeta(accounts.optionAccount),
163
176
  getAccountMeta(accounts.optionPool),
164
177
  getAccountMeta(accounts.marketData),
178
+ getAccountMeta(accounts.volatilityQuoteAccount),
165
179
  getAccountMeta(accounts.keeper),
166
180
  ],
167
181
  data: getUpdateImpliedVolatilityInstructionDataEncoder().encode({}),
@@ -171,6 +185,7 @@ export function getUpdateImpliedVolatilityInstruction<
171
185
  TAccountOptionAccount,
172
186
  TAccountOptionPool,
173
187
  TAccountMarketData,
188
+ TAccountVolatilityQuoteAccount,
174
189
  TAccountKeeper
175
190
  >);
176
191
  }
@@ -187,8 +202,9 @@ export type ParsedUpdateImpliedVolatilityInstruction<
187
202
  optionPool: TAccountMetas[1];
188
203
  /** Market data - provides baseline historical volatility */
189
204
  marketData: TAccountMetas[2];
205
+ volatilityQuoteAccount: TAccountMetas[3];
190
206
  /** Keeper/anyone can call this instruction (permissionless update) */
191
- keeper: TAccountMetas[3];
207
+ keeper: TAccountMetas[4];
192
208
  };
193
209
  data: UpdateImpliedVolatilityInstructionData;
194
210
  };
@@ -201,7 +217,7 @@ export function parseUpdateImpliedVolatilityInstruction<
201
217
  InstructionWithAccounts<TAccountMetas> &
202
218
  InstructionWithData<ReadonlyUint8Array>,
203
219
  ): ParsedUpdateImpliedVolatilityInstruction<TProgram, TAccountMetas> {
204
- if (instruction.accounts.length < 4) {
220
+ if (instruction.accounts.length < 5) {
205
221
  // TODO: Coded error.
206
222
  throw new Error("Not enough accounts");
207
223
  }
@@ -217,6 +233,7 @@ export function parseUpdateImpliedVolatilityInstruction<
217
233
  optionAccount: getNextAccount(),
218
234
  optionPool: getNextAccount(),
219
235
  marketData: getNextAccount(),
236
+ volatilityQuoteAccount: getNextAccount(),
220
237
  keeper: getNextAccount(),
221
238
  },
222
239
  data: getUpdateImpliedVolatilityInstructionDataDecoder().decode(
@@ -34,6 +34,7 @@ import {
34
34
  type ParsedLiquidateWriterPositionInstruction,
35
35
  type ParsedLiquidateWriterPositionRescueInstruction,
36
36
  type ParsedMigrateCollateralPoolV1ToV2Instruction,
37
+ type ParsedMigrateMarketDataVolOracleInstruction,
37
38
  type ParsedOmlpCreateVaultInstruction,
38
39
  type ParsedOmlpUpdateFeeWalletInstruction,
39
40
  type ParsedOmlpUpdateInterestModelInstruction,
@@ -265,6 +266,7 @@ export enum OptionProgramInstruction {
265
266
  LiquidateWriterPosition,
266
267
  LiquidateWriterPositionRescue,
267
268
  MigrateCollateralPoolV1ToV2,
269
+ MigrateMarketDataVolOracle,
268
270
  OmlpCreateVault,
269
271
  OmlpUpdateFeeWallet,
270
272
  OmlpUpdateInterestModel,
@@ -516,6 +518,17 @@ export function identifyOptionProgramInstruction(
516
518
  ) {
517
519
  return OptionProgramInstruction.MigrateCollateralPoolV1ToV2;
518
520
  }
521
+ if (
522
+ containsBytes(
523
+ data,
524
+ fixEncoderSize(getBytesEncoder(), 8).encode(
525
+ new Uint8Array([230, 102, 218, 98, 194, 231, 97, 241]),
526
+ ),
527
+ 0,
528
+ )
529
+ ) {
530
+ return OptionProgramInstruction.MigrateMarketDataVolOracle;
531
+ }
519
532
  if (
520
533
  containsBytes(
521
534
  data,
@@ -859,6 +872,9 @@ export type ParsedOptionProgramInstruction<
859
872
  | ({
860
873
  instructionType: OptionProgramInstruction.MigrateCollateralPoolV1ToV2;
861
874
  } & ParsedMigrateCollateralPoolV1ToV2Instruction<TProgram>)
875
+ | ({
876
+ instructionType: OptionProgramInstruction.MigrateMarketDataVolOracle;
877
+ } & ParsedMigrateMarketDataVolOracleInstruction<TProgram>)
862
878
  | ({
863
879
  instructionType: OptionProgramInstruction.OmlpCreateVault;
864
880
  } & ParsedOmlpCreateVaultInstruction<TProgram>)
package/index.ts CHANGED
@@ -64,6 +64,12 @@ export {
64
64
  type SwitchboardNetwork,
65
65
  } from "./oracle/switchboard";
66
66
 
67
+ export {
68
+ deriveVolatilityQuoteAddress,
69
+ deriveVolatilityQuoteAddressFromMarketData,
70
+ isVolOracleEnabled,
71
+ } from "./oracle/volatility-quote";
72
+
67
73
  export {
68
74
  getWrapSOLInstructions,
69
75
  getUnwrapSOLInstructions,
package/long/builders.ts CHANGED
@@ -36,6 +36,7 @@ import {
36
36
  inferSwitchboardNetwork,
37
37
  prependSwitchboardQuote,
38
38
  } from "../oracle/switchboard";
39
+ import { deriveVolatilityQuoteAddressFromMarketData } from "../oracle/volatility-quote";
39
40
  import { fetchWriterPositionsForPool } from "../accounts/list";
40
41
  import { getGlobalTradeConfig } from "../shared/trade-config";
41
42
 
@@ -45,6 +46,8 @@ export interface BuildBuyFromPoolParams {
45
46
  longMint: AddressLike;
46
47
  underlyingMint: AddressLike;
47
48
  marketData: AddressLike;
49
+ /** Authority-updated Switchboard HV quote PDA (see deriveVolatilityQuoteAddressFromMarketData). */
50
+ volatilityQuoteAccount: AddressLike;
48
51
  /** Program `switchboard_queue` — use {@link getDefaultSwitchboardQueueAddress} for the cluster. */
49
52
  switchboardQueue: AddressLike;
50
53
  buyer: AddressLike;
@@ -92,6 +95,8 @@ export interface BuildCloseLongToPoolParams {
92
95
  escrowLongAccount: AddressLike;
93
96
  premiumVault: AddressLike;
94
97
  marketData: AddressLike;
98
+ /** Authority-updated Switchboard HV quote PDA (see deriveVolatilityQuoteAddressFromMarketData). */
99
+ volatilityQuoteAccount: AddressLike;
95
100
  switchboardQueue: AddressLike;
96
101
  buyer: AddressLike;
97
102
  buyerLongAccount: AddressLike;
@@ -110,10 +115,12 @@ export interface BuildCloseLongToPoolParams {
110
115
  */
111
116
  closeLongTokenAccount?: boolean;
112
117
  /**
113
- * When true and underlying is WSOL, appends an SPL CloseAccount to unwrap the payout ATA so the buyer receives native SOL.
114
- * Ignored when underlyingMint is not WSOL.
118
+ * When true and settlement is physical (collateral mint == underlying) with WSOL underlying,
119
+ * appends CloseAccount to unwrap the payout WSOL ATA to native SOL.
115
120
  */
116
121
  unwrapPayoutSol?: boolean;
122
+ /** When false, skips idempotent create-ATA for payout accounts (default: true). */
123
+ createPayoutAtas?: boolean;
117
124
  remainingAccounts?: RemainingAccountInput[];
118
125
  }
119
126
 
@@ -129,6 +136,7 @@ export async function buildBuyFromPoolInstruction(
129
136
  longMint: toAddress(params.longMint),
130
137
  underlyingMint: toAddress(params.underlyingMint),
131
138
  marketData: toAddress(params.marketData),
139
+ volatilityQuoteAccount: toAddress(params.volatilityQuoteAccount),
132
140
  switchboardQueue: toAddress(params.switchboardQueue),
133
141
  buyer: toAddress(params.buyer) as any,
134
142
  buyerPosition: params.buyerPosition ? toAddress(params.buyerPosition) : undefined,
@@ -215,6 +223,8 @@ export interface BuildBuyFromPoolTransactionWithDerivationParams {
215
223
  * Use **1** if only SetComputeUnitLimit is prepended (e.g. `omitComputeUnitPriceInstruction`); **0** if no CU prepend.
216
224
  */
217
225
  switchboardQuoteInstructionIndex?: number;
226
+ /** Optional override — derived from on-chain market data when omitted. */
227
+ volatilityQuoteAccount?: AddressLike;
218
228
  }
219
229
 
220
230
  const DEFAULT_MARKET_ORDER_SLIPPAGE_BUFFER_BASE_UNITS = 500_000n;
@@ -301,12 +311,22 @@ export async function buildBuyFromPoolTransactionWithDerivation(
301
311
  }),
302
312
  ]);
303
313
 
314
+ const marketDataAccount = await fetchMarketDataAccount(params.rpc, resolved.marketData);
315
+ invariant(
316
+ !!marketDataAccount,
317
+ "Market data account not found for resolved option market."
318
+ );
319
+ const volatilityQuoteAccount =
320
+ params.volatilityQuoteAccount ??
321
+ deriveVolatilityQuoteAddressFromMarketData(marketDataAccount);
322
+
304
323
  const buyParams = {
305
324
  optionPool: resolved.optionPool,
306
325
  optionAccount: resolved.optionAccount,
307
326
  longMint: resolved.longMint,
308
327
  underlyingMint: resolved.underlyingMint!,
309
328
  marketData: resolved.marketData,
329
+ volatilityQuoteAccount,
310
330
  buyer: params.buyer,
311
331
  paymentMint: paymentAccounts.paymentMint,
312
332
  buyerPaymentAccount: paymentAccounts.buyerPaymentAccount,
@@ -319,11 +339,6 @@ export async function buildBuyFromPoolTransactionWithDerivation(
319
339
  remainingAccounts: params.remainingAccounts,
320
340
  };
321
341
 
322
- const marketDataAccount = await fetchMarketDataAccount(params.rpc, resolved.marketData);
323
- invariant(
324
- !!marketDataAccount,
325
- "Market data account not found for resolved option market."
326
- );
327
342
  const feedIdHex = feedIdBytesToHex(
328
343
  Uint8Array.from(marketDataAccount.switchboardFeedId as unknown as Uint8Array)
329
344
  );
@@ -458,12 +473,22 @@ export async function buildBuyFromPoolMarketOrderTransactionWithDerivation(
458
473
  const maxPremiumAmount = quotePremium + slippageBuffer;
459
474
  assertPositiveAmount(maxPremiumAmount, "maxPremiumAmount");
460
475
 
476
+ const marketDataAccount = await fetchMarketDataAccount(params.rpc, resolved.marketData);
477
+ invariant(
478
+ !!marketDataAccount,
479
+ "Market data account not found for resolved option market."
480
+ );
481
+ const volatilityQuoteAccount =
482
+ params.volatilityQuoteAccount ??
483
+ deriveVolatilityQuoteAddressFromMarketData(marketDataAccount);
484
+
461
485
  const marketBuyParams = {
462
486
  optionPool: resolved.optionPool,
463
487
  optionAccount: resolved.optionAccount,
464
488
  longMint: resolved.longMint,
465
489
  underlyingMint: refetchedPool.underlyingMint,
466
490
  marketData: resolved.marketData,
491
+ volatilityQuoteAccount,
467
492
  buyer: params.buyer,
468
493
  paymentMint: paymentAccounts.paymentMint,
469
494
  buyerPaymentAccount: paymentAccounts.buyerPaymentAccount,
@@ -476,11 +501,6 @@ export async function buildBuyFromPoolMarketOrderTransactionWithDerivation(
476
501
  remainingAccounts,
477
502
  };
478
503
 
479
- const marketDataAccount = await fetchMarketDataAccount(params.rpc, resolved.marketData);
480
- invariant(
481
- !!marketDataAccount,
482
- "Market data account not found for resolved option market."
483
- );
484
504
  const feedIdHex = feedIdBytesToHex(
485
505
  Uint8Array.from(marketDataAccount.switchboardFeedId as unknown as Uint8Array)
486
506
  );
@@ -539,6 +559,7 @@ export async function buildCloseLongToPoolInstruction(
539
559
  escrowLongAccount: toAddress(params.escrowLongAccount),
540
560
  premiumVault: toAddress(params.premiumVault),
541
561
  marketData: toAddress(params.marketData),
562
+ volatilityQuoteAccount: toAddress(params.volatilityQuoteAccount),
542
563
  switchboardQueue: toAddress(params.switchboardQueue),
543
564
  buyer: toAddress(params.buyer) as any,
544
565
  buyerLongAccount: toAddress(params.buyerLongAccount),
@@ -554,10 +575,36 @@ export async function buildCloseLongToPoolInstruction(
554
575
  }
555
576
 
556
577
  export async function buildCloseLongToPoolTransaction(
557
- params: BuildCloseLongToPoolParams
578
+ params: BuildCloseLongToPoolParams & { createPayoutAtas?: boolean }
558
579
  ): Promise<BuiltTransaction> {
580
+ const collateralMint = params.collateralMint ?? params.underlyingMint;
581
+ const isPhysicalSettlement =
582
+ toAddress(collateralMint) === toAddress(params.underlyingMint);
583
+ const instructions: Instruction<string>[] = [];
584
+
585
+ if (params.createPayoutAtas !== false) {
586
+ instructions.push(
587
+ await getCreateAssociatedTokenIdempotentInstructionWithAddress(
588
+ params.buyer,
589
+ params.buyer,
590
+ toAddress(collateralMint),
591
+ toAddress(params.buyerPayoutAccount)
592
+ )
593
+ );
594
+ if (!isPhysicalSettlement) {
595
+ instructions.push(
596
+ await getCreateAssociatedTokenIdempotentInstructionWithAddress(
597
+ params.buyer,
598
+ params.buyer,
599
+ toAddress(params.underlyingMint),
600
+ toAddress(params.buyerUnderlyingPayoutAccount)
601
+ )
602
+ );
603
+ }
604
+ }
605
+
559
606
  const instruction = await buildCloseLongToPoolInstruction(params);
560
- const instructions = [instruction];
607
+ instructions.push(instruction);
561
608
 
562
609
  if (params.closeLongTokenAccount === true) {
563
610
  instructions.push(
@@ -609,10 +656,12 @@ export interface BuildCloseLongToPoolTransactionWithDerivationParams {
609
656
  */
610
657
  closeLongTokenAccount?: boolean;
611
658
  /**
612
- * When true (default for WSOL underlying), appends CloseAccount to unwrap payout WSOL ATA to native SOL.
613
- * Only applies when option underlying is WSOL.
659
+ * When true, unwraps physical-settlement WSOL payout to native SOL after close.
660
+ * Defaults to true only for physical WSOL pools; cash-settled pools default to false.
614
661
  */
615
662
  unwrapPayoutSol?: boolean;
663
+ /** When false, skips idempotent create-ATA for payout accounts (default: true). */
664
+ createPayoutAtas?: boolean;
616
665
  remainingAccounts?: RemainingAccountInput[];
617
666
  disableSwitchboardCrank?: boolean;
618
667
  switchboardCrossbarUrl?: string;
@@ -622,6 +671,8 @@ export interface BuildCloseLongToPoolTransactionWithDerivationParams {
622
671
  * OPX `sendInstructions` prepends only `SetComputeUnitLimit` before program ixs → default **1** (quote after CU limit).
623
672
  */
624
673
  switchboardQuoteInstructionIndex?: number;
674
+ /** Optional override — derived from on-chain market data when omitted. */
675
+ volatilityQuoteAccount?: AddressLike;
625
676
  }
626
677
 
627
678
  export async function buildCloseLongToPoolTransactionWithDerivation(
@@ -665,10 +716,13 @@ export async function buildCloseLongToPoolTransactionWithDerivation(
665
716
 
666
717
  const isWsolUnderlying =
667
718
  toAddress(resolved.underlyingMint!) === toAddress(NATIVE_MINT);
719
+ const isPhysicalSettlement =
720
+ toAddress(collateralMint) === toAddress(resolved.underlyingMint!);
668
721
  const closeLongTokenAccount =
669
722
  params.closeLongTokenAccount !== false;
670
723
  const unwrapPayoutSol =
671
- params.unwrapPayoutSol !== false && isWsolUnderlying;
724
+ params.unwrapPayoutSol ??
725
+ (isWsolUnderlying && isPhysicalSettlement);
672
726
 
673
727
  // close_long_to_pool requires a complete set of active WriterPositions so the
674
728
  // program can run the strict Hamilton completeness check. Auto-populate if
@@ -686,6 +740,9 @@ export async function buildCloseLongToPoolTransactionWithDerivation(
686
740
  !!marketDataAccount,
687
741
  "Market data account not found for resolved option market."
688
742
  );
743
+ const volatilityQuoteAccount =
744
+ params.volatilityQuoteAccount ??
745
+ deriveVolatilityQuoteAddressFromMarketData(marketDataAccount);
689
746
  const feedIdHex = feedIdBytesToHex(
690
747
  Uint8Array.from(marketDataAccount.switchboardFeedId as unknown as Uint8Array)
691
748
  );
@@ -705,6 +762,7 @@ export async function buildCloseLongToPoolTransactionWithDerivation(
705
762
  escrowLongAccount: resolved.escrowLongAccount!,
706
763
  premiumVault: resolved.premiumVault!,
707
764
  marketData: resolved.marketData,
765
+ volatilityQuoteAccount,
708
766
  switchboardQueue,
709
767
  buyer: params.buyer,
710
768
  buyerLongAccount: params.buyerLongAccount,
@@ -716,6 +774,7 @@ export async function buildCloseLongToPoolTransactionWithDerivation(
716
774
  buyerPosition,
717
775
  closeLongTokenAccount,
718
776
  unwrapPayoutSol,
777
+ createPayoutAtas: params.createPayoutAtas,
719
778
  remainingAccounts,
720
779
  });
721
780
  }
@@ -744,6 +803,7 @@ export async function buildCloseLongToPoolTransactionWithDerivation(
744
803
  escrowLongAccount: resolved.escrowLongAccount!,
745
804
  premiumVault: resolved.premiumVault!,
746
805
  marketData: resolved.marketData,
806
+ volatilityQuoteAccount,
747
807
  switchboardQueue: getDefaultSwitchboardQueueAddress(network),
748
808
  buyer: params.buyer,
749
809
  buyerLongAccount: params.buyerLongAccount,
@@ -755,6 +815,7 @@ export async function buildCloseLongToPoolTransactionWithDerivation(
755
815
  buyerPosition,
756
816
  closeLongTokenAccount,
757
817
  unwrapPayoutSol,
818
+ createPayoutAtas: params.createPayoutAtas,
758
819
  remainingAccounts,
759
820
  });
760
821
 
@@ -0,0 +1,65 @@
1
+ import { PublicKey } from "@solana/web3.js";
2
+ import { address, type Address } from "@solana/kit";
3
+ import { toAddress } from "../client/program";
4
+ import type { AddressLike } from "../client/types";
5
+ import type { MarketDataAccount } from "../generated/accounts/marketDataAccount";
6
+
7
+ const ZERO_PUBKEY = "11111111111111111111111111111111";
8
+ const QUOTE_PROGRAM_ID = new PublicKey("orac1eFjzWL5R3RbbdMV68K9H6TaCVVcL6LjvQQWAbz");
9
+ const AUTHORITY_QUOTE_SCHEME_TAG = Buffer.from("AUTH");
10
+
11
+ function feedIdBytes(feedId: MarketDataAccount["switchboardVolatilityFeedId"]): Uint8Array {
12
+ return Uint8Array.from(feedId);
13
+ }
14
+
15
+ function feedIdIsZero(feedId: MarketDataAccount["switchboardVolatilityFeedId"]): boolean {
16
+ const bytes = feedIdBytes(feedId);
17
+ return bytes.length === 32 && bytes.every((b) => b === 0);
18
+ }
19
+
20
+ export function isVolOracleEnabled(
21
+ marketData: Pick<
22
+ MarketDataAccount,
23
+ "switchboardVolatilityFeedId" | "volatilityQuoteAuthority"
24
+ >
25
+ ): boolean {
26
+ return (
27
+ !feedIdIsZero(marketData.switchboardVolatilityFeedId) &&
28
+ String(marketData.volatilityQuoteAuthority) !== ZERO_PUBKEY
29
+ );
30
+ }
31
+
32
+ /**
33
+ * Derives the Switchboard authority quote PDA for an HV feed.
34
+ */
35
+ export function deriveVolatilityQuoteAddress(
36
+ authority: AddressLike,
37
+ switchboardVolatilityFeedId: MarketDataAccount["switchboardVolatilityFeedId"]
38
+ ): Address {
39
+ const authorityPk = new PublicKey(String(toAddress(authority)));
40
+ const feedHash = Buffer.from(feedIdBytes(switchboardVolatilityFeedId));
41
+ const [quoteAccount] = PublicKey.findProgramAddressSync(
42
+ [AUTHORITY_QUOTE_SCHEME_TAG, authorityPk.toBuffer(), feedHash],
43
+ QUOTE_PROGRAM_ID
44
+ );
45
+ return address(quoteAccount.toBase58());
46
+ }
47
+
48
+ /**
49
+ * When vol oracle is disabled on market data, returns the system program as a
50
+ * read-only placeholder (the program skips quote reads in that case).
51
+ */
52
+ export function deriveVolatilityQuoteAddressFromMarketData(
53
+ marketData: Pick<
54
+ MarketDataAccount,
55
+ "volatilityQuoteAuthority" | "switchboardVolatilityFeedId"
56
+ >
57
+ ): Address {
58
+ if (!isVolOracleEnabled(marketData)) {
59
+ return address(ZERO_PUBKEY);
60
+ }
61
+ return deriveVolatilityQuoteAddress(
62
+ marketData.volatilityQuoteAuthority,
63
+ marketData.switchboardVolatilityFeedId
64
+ );
65
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@epicentral/sos-sdk",
3
- "version": "0.12.1-beta",
3
+ "version": "0.13.0-beta",
4
4
  "private": false,
5
5
  "description": "Solana Option Standard SDK. The frontend-first SDK for Native Options Trading on Solana. Created by Epicentral Labs.",
6
6
  "type": "module",
@@ -20,11 +20,17 @@
20
20
  "@solana-program/system": "^0.11.0",
21
21
  "@solana/compat": "^6.1.0",
22
22
  "@solana/kit": "^6.1.0",
23
+ "@solana/web3.js": "^1.98.0",
23
24
  "@switchboard-xyz/common": "^5.7.0",
24
25
  "@switchboard-xyz/on-demand": "^3.9.0",
25
26
  "bs58": "^6.0.0",
26
27
  "decimal.js": "^10.4.3"
27
28
  },
29
+ "overrides": {
30
+ "@switchboard-xyz/on-demand": {
31
+ "@switchboard-xyz/common": "$@switchboard-xyz/common"
32
+ }
33
+ },
28
34
  "scripts": {
29
35
  "typecheck": "tsc --project tsconfig.json --noEmit",
30
36
  "publish-beta": "dotenv -e .env -- pnpm publish --access public --tag beta",
@@ -26,6 +26,7 @@ import {
26
26
  parseLiquidateWriterPositionInstruction,
27
27
  parseLiquidateWriterPositionRescueInstruction,
28
28
  parseMigrateCollateralPoolV1ToV2Instruction,
29
+ parseMigrateMarketDataVolOracleInstruction,
29
30
  parseOmlpCreateVaultInstruction,
30
31
  parseOmlpUpdateFeeWalletInstruction,
31
32
  parseOmlpUpdateInterestModelInstruction,
@@ -179,6 +180,11 @@ export function parseOptionProgramInstruction<
179
180
  parseLiquidateWriterPositionRescueInstruction(instruction),
180
181
  instructionType,
181
182
  );
183
+ case OptionProgramInstruction.MigrateMarketDataVolOracle:
184
+ return withInstructionType(
185
+ parseMigrateMarketDataVolOracleInstruction(instruction),
186
+ instructionType,
187
+ );
182
188
  case OptionProgramInstruction.MigrateCollateralPoolV1ToV2:
183
189
  return withInstructionType(
184
190
  parseMigrateCollateralPoolV1ToV2Instruction(instruction),
package/short/builders.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  getClaimBuyerSettlementInstructionAsync,
3
3
  getClaimMakerSettlementInstructionAsync,
4
+ getLiquidateWriterPositionInstructionAsync,
4
5
  getLiquidateWriterPositionRescueInstructionAsync,
5
6
  getOptionMintInstructionAsync,
6
7
  getPrepareBuyerSettlementInstructionAsync,
@@ -42,6 +43,7 @@ import {
42
43
  SLOT_HASHES_SYSVAR_ADDRESS,
43
44
  SWITCHBOARD_DEFAULT_DEVNET_QUEUE,
44
45
  } from "../oracle/switchboard";
46
+ import { deriveVolatilityQuoteAddressFromMarketData } from "../oracle/volatility-quote";
45
47
  import { applySlippageBps } from "../long/quotes";
46
48
  import { getGlobalTradeConfig } from "../shared/trade-config";
47
49
 
@@ -75,6 +77,8 @@ export interface BuildOptionMintParams {
75
77
  makerLongAccount?: AddressLike;
76
78
  makerShortAccount?: AddressLike;
77
79
  marketData?: AddressLike;
80
+ /** Authority-updated Switchboard HV quote PDA (derived from market data when omitted). */
81
+ volatilityQuoteAccount?: AddressLike;
78
82
  optionPool?: AddressLike;
79
83
  escrowLongAccount?: AddressLike;
80
84
  premiumVault?: AddressLike;
@@ -240,6 +244,10 @@ export async function buildOptionMintInstruction(
240
244
  !!longMetadata && !!shortMetadata,
241
245
  "longMetadataAccount and shortMetadataAccount are required (or provide longMint/shortMint to derive)."
242
246
  );
247
+ invariant(
248
+ !!params.volatilityQuoteAccount,
249
+ "volatilityQuoteAccount is required (use buildOptionMintTransactionWithDerivation or deriveVolatilityQuoteAddressFromMarketData)."
250
+ );
243
251
 
244
252
  const kitInstruction = await getOptionMintInstructionAsync(
245
253
  {
@@ -256,6 +264,7 @@ export async function buildOptionMintInstruction(
256
264
  longMetadataAccount: toAddress(longMetadata!),
257
265
  shortMetadataAccount: toAddress(shortMetadata!),
258
266
  marketData: params.marketData ? toAddress(params.marketData) : undefined,
267
+ volatilityQuoteAccount: toAddress(params.volatilityQuoteAccount!),
259
268
  underlyingMint: toAddress(params.underlyingMint),
260
269
  collateralMint: toAddress(params.collateralMint ?? params.underlyingMint),
261
270
  optionPool: params.optionPool ? toAddress(params.optionPool) : undefined,
@@ -383,6 +392,8 @@ export interface BuildOptionMintTransactionWithDerivationParams {
383
392
  * under Solana's 1232-byte versioned-transaction limit when OMLP accounts are present.
384
393
  */
385
394
  skipCreateCollateralAta?: boolean;
395
+ /** Optional override — derived from on-chain market data when omitted. */
396
+ volatilityQuoteAccount?: AddressLike;
386
397
  }
387
398
 
388
399
  export async function buildOptionMintTransactionWithDerivation(
@@ -447,6 +458,9 @@ export async function buildOptionMintTransactionWithDerivation(
447
458
  !!marketDataAccount,
448
459
  "Market data account not found for resolved option market."
449
460
  );
461
+ const volatilityQuoteAccount =
462
+ params.volatilityQuoteAccount ??
463
+ deriveVolatilityQuoteAddressFromMarketData(marketDataAccount);
450
464
  const switchboardFeedId = feedIdBytesToHex(
451
465
  Uint8Array.from(marketDataAccount.switchboardFeedId as unknown as Uint8Array)
452
466
  );
@@ -514,6 +528,7 @@ export async function buildOptionMintTransactionWithDerivation(
514
528
  makerLongAccount,
515
529
  makerShortAccount,
516
530
  marketData: resolved.marketData,
531
+ volatilityQuoteAccount,
517
532
  optionPool: resolved.optionPool,
518
533
  escrowLongAccount: resolved.escrowLongAccount,
519
534
  premiumVault: resolved.premiumVault,
@@ -957,6 +972,84 @@ export async function buildClaimMakerSettlementInstruction(
957
972
  });
958
973
  }
959
974
 
975
+ export interface BuildLiquidateWriterPositionParams {
976
+ optionPool: AddressLike;
977
+ optionAccount: AddressLike;
978
+ /** Market data PDA (`["market_data", underlying_asset]`). */
979
+ marketData: AddressLike;
980
+ /** Authority-updated Switchboard HV quote PDA (derived from market data when omitted). */
981
+ volatilityQuoteAccount?: AddressLike;
982
+ collateralPool: AddressLike;
983
+ writerPosition: AddressLike;
984
+ longMint: AddressLike;
985
+ escrowLongAccount: AddressLike;
986
+ underlyingMint: AddressLike;
987
+ switchboardQueue: AddressLike;
988
+ /** OMLP Vault state PDA (mut: decrements total_loans, etc.). */
989
+ omlpVault: AddressLike;
990
+ collateralVault: AddressLike;
991
+ premiumVault: AddressLike;
992
+ omlpVaultTokenAccount: AddressLike;
993
+ feeWallet: AddressLike;
994
+ /** Permissionless liquidator signer. */
995
+ liquidator: TransactionSigner<string>;
996
+ /** Every active PoolLoan for this writer/vault (SDK: loadWriterLoans). */
997
+ remainingAccounts?: RemainingAccountInput[];
998
+ rpc?: KitRpc;
999
+ }
1000
+
1001
+ export async function buildLiquidateWriterPositionInstruction(
1002
+ params: BuildLiquidateWriterPositionParams
1003
+ ): Promise<Instruction<string>> {
1004
+ let volatilityQuoteAccount = params.volatilityQuoteAccount;
1005
+ if (!volatilityQuoteAccount) {
1006
+ invariant(!!params.rpc, "rpc is required when volatilityQuoteAccount is omitted");
1007
+ const marketDataAccount = await fetchMarketDataAccount(params.rpc!, params.marketData);
1008
+ invariant(
1009
+ !!marketDataAccount,
1010
+ "Market data account not found for resolved option market."
1011
+ );
1012
+ volatilityQuoteAccount = deriveVolatilityQuoteAddressFromMarketData(marketDataAccount);
1013
+ }
1014
+
1015
+ const kitInstruction = await getLiquidateWriterPositionInstructionAsync({
1016
+ optionPool: toAddress(params.optionPool),
1017
+ optionAccount: toAddress(params.optionAccount),
1018
+ marketData: toAddress(params.marketData),
1019
+ volatilityQuoteAccount: toAddress(volatilityQuoteAccount),
1020
+ collateralPool: toAddress(params.collateralPool),
1021
+ writerPosition: toAddress(params.writerPosition),
1022
+ longMint: toAddress(params.longMint),
1023
+ escrowLongAccount: toAddress(params.escrowLongAccount),
1024
+ underlyingMint: toAddress(params.underlyingMint),
1025
+ switchboardQueue: toAddress(params.switchboardQueue),
1026
+ omlpVault: toAddress(params.omlpVault),
1027
+ collateralVault: toAddress(params.collateralVault),
1028
+ premiumVault: toAddress(params.premiumVault),
1029
+ omlpVaultTokenAccount: toAddress(params.omlpVaultTokenAccount),
1030
+ feeWallet: toAddress(params.feeWallet),
1031
+ liquidator: params.liquidator as any,
1032
+ });
1033
+ return appendRemainingAccounts(kitInstruction, [
1034
+ {
1035
+ address: SLOT_HASHES_SYSVAR_ADDRESS,
1036
+ isWritable: false,
1037
+ },
1038
+ {
1039
+ address: INSTRUCTIONS_SYSVAR_ADDRESS,
1040
+ isWritable: false,
1041
+ },
1042
+ ...(params.remainingAccounts ?? []),
1043
+ ]);
1044
+ }
1045
+
1046
+ export async function buildLiquidateWriterPositionTransaction(
1047
+ params: BuildLiquidateWriterPositionParams
1048
+ ): Promise<BuiltTransaction> {
1049
+ const instruction = await buildLiquidateWriterPositionInstruction(params);
1050
+ return { instructions: [instruction] };
1051
+ }
1052
+
960
1053
  /**
961
1054
  * Parameters for the permissioned rescue liquidation path. Gated on-chain
962
1055
  * to `Vault::keeper`; the SDK does not re-verify keeper identity, but the