@epicentral/sos-sdk 0.12.2-beta → 0.13.1-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.
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;
@@ -131,6 +136,7 @@ export async function buildBuyFromPoolInstruction(
131
136
  longMint: toAddress(params.longMint),
132
137
  underlyingMint: toAddress(params.underlyingMint),
133
138
  marketData: toAddress(params.marketData),
139
+ volatilityQuoteAccount: toAddress(params.volatilityQuoteAccount),
134
140
  switchboardQueue: toAddress(params.switchboardQueue),
135
141
  buyer: toAddress(params.buyer) as any,
136
142
  buyerPosition: params.buyerPosition ? toAddress(params.buyerPosition) : undefined,
@@ -217,6 +223,8 @@ export interface BuildBuyFromPoolTransactionWithDerivationParams {
217
223
  * Use **1** if only SetComputeUnitLimit is prepended (e.g. `omitComputeUnitPriceInstruction`); **0** if no CU prepend.
218
224
  */
219
225
  switchboardQuoteInstructionIndex?: number;
226
+ /** Optional override — derived from on-chain market data when omitted. */
227
+ volatilityQuoteAccount?: AddressLike;
220
228
  }
221
229
 
222
230
  const DEFAULT_MARKET_ORDER_SLIPPAGE_BUFFER_BASE_UNITS = 500_000n;
@@ -303,12 +311,22 @@ export async function buildBuyFromPoolTransactionWithDerivation(
303
311
  }),
304
312
  ]);
305
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
+
306
323
  const buyParams = {
307
324
  optionPool: resolved.optionPool,
308
325
  optionAccount: resolved.optionAccount,
309
326
  longMint: resolved.longMint,
310
327
  underlyingMint: resolved.underlyingMint!,
311
328
  marketData: resolved.marketData,
329
+ volatilityQuoteAccount,
312
330
  buyer: params.buyer,
313
331
  paymentMint: paymentAccounts.paymentMint,
314
332
  buyerPaymentAccount: paymentAccounts.buyerPaymentAccount,
@@ -321,11 +339,6 @@ export async function buildBuyFromPoolTransactionWithDerivation(
321
339
  remainingAccounts: params.remainingAccounts,
322
340
  };
323
341
 
324
- const marketDataAccount = await fetchMarketDataAccount(params.rpc, resolved.marketData);
325
- invariant(
326
- !!marketDataAccount,
327
- "Market data account not found for resolved option market."
328
- );
329
342
  const feedIdHex = feedIdBytesToHex(
330
343
  Uint8Array.from(marketDataAccount.switchboardFeedId as unknown as Uint8Array)
331
344
  );
@@ -460,12 +473,22 @@ export async function buildBuyFromPoolMarketOrderTransactionWithDerivation(
460
473
  const maxPremiumAmount = quotePremium + slippageBuffer;
461
474
  assertPositiveAmount(maxPremiumAmount, "maxPremiumAmount");
462
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
+
463
485
  const marketBuyParams = {
464
486
  optionPool: resolved.optionPool,
465
487
  optionAccount: resolved.optionAccount,
466
488
  longMint: resolved.longMint,
467
489
  underlyingMint: refetchedPool.underlyingMint,
468
490
  marketData: resolved.marketData,
491
+ volatilityQuoteAccount,
469
492
  buyer: params.buyer,
470
493
  paymentMint: paymentAccounts.paymentMint,
471
494
  buyerPaymentAccount: paymentAccounts.buyerPaymentAccount,
@@ -478,11 +501,6 @@ export async function buildBuyFromPoolMarketOrderTransactionWithDerivation(
478
501
  remainingAccounts,
479
502
  };
480
503
 
481
- const marketDataAccount = await fetchMarketDataAccount(params.rpc, resolved.marketData);
482
- invariant(
483
- !!marketDataAccount,
484
- "Market data account not found for resolved option market."
485
- );
486
504
  const feedIdHex = feedIdBytesToHex(
487
505
  Uint8Array.from(marketDataAccount.switchboardFeedId as unknown as Uint8Array)
488
506
  );
@@ -541,6 +559,7 @@ export async function buildCloseLongToPoolInstruction(
541
559
  escrowLongAccount: toAddress(params.escrowLongAccount),
542
560
  premiumVault: toAddress(params.premiumVault),
543
561
  marketData: toAddress(params.marketData),
562
+ volatilityQuoteAccount: toAddress(params.volatilityQuoteAccount),
544
563
  switchboardQueue: toAddress(params.switchboardQueue),
545
564
  buyer: toAddress(params.buyer) as any,
546
565
  buyerLongAccount: toAddress(params.buyerLongAccount),
@@ -652,6 +671,8 @@ export interface BuildCloseLongToPoolTransactionWithDerivationParams {
652
671
  * OPX `sendInstructions` prepends only `SetComputeUnitLimit` before program ixs → default **1** (quote after CU limit).
653
672
  */
654
673
  switchboardQuoteInstructionIndex?: number;
674
+ /** Optional override — derived from on-chain market data when omitted. */
675
+ volatilityQuoteAccount?: AddressLike;
655
676
  }
656
677
 
657
678
  export async function buildCloseLongToPoolTransactionWithDerivation(
@@ -719,6 +740,9 @@ export async function buildCloseLongToPoolTransactionWithDerivation(
719
740
  !!marketDataAccount,
720
741
  "Market data account not found for resolved option market."
721
742
  );
743
+ const volatilityQuoteAccount =
744
+ params.volatilityQuoteAccount ??
745
+ deriveVolatilityQuoteAddressFromMarketData(marketDataAccount);
722
746
  const feedIdHex = feedIdBytesToHex(
723
747
  Uint8Array.from(marketDataAccount.switchboardFeedId as unknown as Uint8Array)
724
748
  );
@@ -738,6 +762,7 @@ export async function buildCloseLongToPoolTransactionWithDerivation(
738
762
  escrowLongAccount: resolved.escrowLongAccount!,
739
763
  premiumVault: resolved.premiumVault!,
740
764
  marketData: resolved.marketData,
765
+ volatilityQuoteAccount,
741
766
  switchboardQueue,
742
767
  buyer: params.buyer,
743
768
  buyerLongAccount: params.buyerLongAccount,
@@ -778,6 +803,7 @@ export async function buildCloseLongToPoolTransactionWithDerivation(
778
803
  escrowLongAccount: resolved.escrowLongAccount!,
779
804
  premiumVault: resolved.premiumVault!,
780
805
  marketData: resolved.marketData,
806
+ volatilityQuoteAccount,
781
807
  switchboardQueue: getDefaultSwitchboardQueueAddress(network),
782
808
  buyer: params.buyer,
783
809
  buyerLongAccount: params.buyerLongAccount,
@@ -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.2-beta",
3
+ "version": "0.13.1-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