@epicentral/sos-sdk 0.9.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.
Files changed (113) hide show
  1. package/.env.example +1 -0
  2. package/AGENTS.md +7 -0
  3. package/LICENSE +21 -0
  4. package/README.md +568 -0
  5. package/accounts/fetchers.ts +196 -0
  6. package/accounts/list.ts +184 -0
  7. package/accounts/pdas.ts +325 -0
  8. package/accounts/resolve-option.ts +104 -0
  9. package/client/lookup-table.ts +114 -0
  10. package/client/program.ts +13 -0
  11. package/client/types.ts +9 -0
  12. package/generated/accounts/collateralPool.ts +217 -0
  13. package/generated/accounts/config.ts +156 -0
  14. package/generated/accounts/escrowState.ts +183 -0
  15. package/generated/accounts/index.ts +20 -0
  16. package/generated/accounts/lenderPosition.ts +211 -0
  17. package/generated/accounts/makerCollateralShare.ts +229 -0
  18. package/generated/accounts/marketDataAccount.ts +176 -0
  19. package/generated/accounts/optionAccount.ts +247 -0
  20. package/generated/accounts/optionPool.ts +285 -0
  21. package/generated/accounts/poolLoan.ts +232 -0
  22. package/generated/accounts/positionAccount.ts +201 -0
  23. package/generated/accounts/vault.ts +366 -0
  24. package/generated/accounts/writerPosition.ts +327 -0
  25. package/generated/errors/index.ts +9 -0
  26. package/generated/errors/optionProgram.ts +476 -0
  27. package/generated/index.ts +13 -0
  28. package/generated/instructions/acceptAdmin.ts +230 -0
  29. package/generated/instructions/autoExerciseAllExpired.ts +685 -0
  30. package/generated/instructions/autoExerciseExpired.ts +754 -0
  31. package/generated/instructions/borrowFromPool.ts +619 -0
  32. package/generated/instructions/buyFromPool.ts +761 -0
  33. package/generated/instructions/closeLongToPool.ts +762 -0
  34. package/generated/instructions/closeOption.ts +235 -0
  35. package/generated/instructions/createEscrowV2.ts +518 -0
  36. package/generated/instructions/depositCollateral.ts +624 -0
  37. package/generated/instructions/depositToPosition.ts +429 -0
  38. package/generated/instructions/index.ts +47 -0
  39. package/generated/instructions/initCollateralPool.ts +513 -0
  40. package/generated/instructions/initConfig.ts +279 -0
  41. package/generated/instructions/initOptionPool.ts +587 -0
  42. package/generated/instructions/initializeMarketData.ts +359 -0
  43. package/generated/instructions/liquidateWriterPosition.ts +750 -0
  44. package/generated/instructions/liquidateWriterPositionRescue.ts +623 -0
  45. package/generated/instructions/omlpCreateVault.ts +553 -0
  46. package/generated/instructions/omlpUpdateFeeWallet.ts +473 -0
  47. package/generated/instructions/omlpUpdateInterestModel.ts +322 -0
  48. package/generated/instructions/omlpUpdateLiquidationThreshold.ts +304 -0
  49. package/generated/instructions/omlpUpdateMaintenanceBuffer.ts +304 -0
  50. package/generated/instructions/omlpUpdateMaxBorrowCap.ts +304 -0
  51. package/generated/instructions/omlpUpdateMaxLeverage.ts +304 -0
  52. package/generated/instructions/omlpUpdateProtocolFee.ts +304 -0
  53. package/generated/instructions/omlpUpdateSupplyLimit.ts +304 -0
  54. package/generated/instructions/optionExercise.ts +617 -0
  55. package/generated/instructions/optionMint.ts +1373 -0
  56. package/generated/instructions/optionValidate.ts +302 -0
  57. package/generated/instructions/repayPoolLoan.ts +558 -0
  58. package/generated/instructions/repayPoolLoanFromCollateral.ts +514 -0
  59. package/generated/instructions/repayPoolLoanFromWallet.ts +542 -0
  60. package/generated/instructions/settleMakerCollateral.ts +509 -0
  61. package/generated/instructions/syncWriterPosition.ts +206 -0
  62. package/generated/instructions/transferAdmin.ts +245 -0
  63. package/generated/instructions/unwindWriterUnsold.ts +764 -0
  64. package/generated/instructions/updateImpliedVolatility.ts +226 -0
  65. package/generated/instructions/updateMarketData.ts +315 -0
  66. package/generated/instructions/withdrawFromPosition.ts +405 -0
  67. package/generated/instructions/writeToPool.ts +619 -0
  68. package/generated/programs/index.ts +9 -0
  69. package/generated/programs/optionProgram.ts +1144 -0
  70. package/generated/shared/index.ts +164 -0
  71. package/generated/types/impliedVolatilityUpdated.ts +73 -0
  72. package/generated/types/index.ts +28 -0
  73. package/generated/types/liquidationExecuted.ts +73 -0
  74. package/generated/types/liquidationRescueEvent.ts +82 -0
  75. package/generated/types/marketDataInitialized.ts +61 -0
  76. package/generated/types/marketDataUpdated.ts +69 -0
  77. package/generated/types/optionClosed.ts +56 -0
  78. package/generated/types/optionExercised.ts +62 -0
  79. package/generated/types/optionExpired.ts +49 -0
  80. package/generated/types/optionMinted.ts +78 -0
  81. package/generated/types/optionType.ts +38 -0
  82. package/generated/types/optionValidated.ts +82 -0
  83. package/generated/types/poolLoanCreated.ts +74 -0
  84. package/generated/types/poolLoanRepaid.ts +74 -0
  85. package/generated/types/positionDeposited.ts +73 -0
  86. package/generated/types/positionWithdrawn.ts +81 -0
  87. package/generated/types/protocolFeeUpdated.ts +69 -0
  88. package/generated/types/vaultCreated.ts +60 -0
  89. package/generated/types/vaultFeeWalletUpdated.ts +67 -0
  90. package/generated/types/vaultInterestModelUpdated.ts +77 -0
  91. package/generated/types/vaultLiquidationThresholdUpdated.ts +69 -0
  92. package/index.ts +68 -0
  93. package/long/builders.ts +690 -0
  94. package/long/exercise.ts +123 -0
  95. package/long/preflight.ts +214 -0
  96. package/long/quotes.ts +48 -0
  97. package/long/remaining-accounts.ts +111 -0
  98. package/omlp/builders.ts +94 -0
  99. package/omlp/service.ts +136 -0
  100. package/oracle/switchboard.ts +315 -0
  101. package/package.json +34 -0
  102. package/shared/amounts.ts +53 -0
  103. package/shared/balances.ts +57 -0
  104. package/shared/errors.ts +12 -0
  105. package/shared/remaining-accounts.ts +41 -0
  106. package/shared/trade-config.ts +27 -0
  107. package/shared/transactions.ts +121 -0
  108. package/short/builders.ts +874 -0
  109. package/short/close-option.ts +34 -0
  110. package/short/pool.ts +189 -0
  111. package/short/preflight.ts +619 -0
  112. package/tsconfig.json +13 -0
  113. package/wsol/instructions.ts +247 -0
@@ -0,0 +1,136 @@
1
+ /**
2
+ * OMLP (Option Maker Liquidity Pool) service – V2 pool-based API only.
3
+ * Exposes: depositToPosition, withdrawFromPosition, withdrawAllFromPosition, withdrawInterestFromPosition.
4
+ * Borrow/repay use short/pool (buildBorrowFromPool*, buildRepayPoolLoan*). Legacy offer-based instructions are not exposed.
5
+ */
6
+ import { fetchLenderPosition, fetchVault } from "../accounts/fetchers";
7
+ import type { KitRpc } from "../client/types";
8
+ import {
9
+ buildDepositToPositionTransaction,
10
+ buildWithdrawFromPositionTransaction,
11
+ type BuildDepositToPositionParams,
12
+ type BuildWithdrawFromPositionParams,
13
+ } from "./builders";
14
+
15
+ const INTEREST_FP_SCALE = 1_000_000_000_000n;
16
+
17
+ function positiveDiff(a: bigint, b: bigint): bigint {
18
+ return a > b ? a - b : 0n;
19
+ }
20
+
21
+ function calculatePendingInterest(
22
+ deposited: bigint,
23
+ vaultAccInterestPerShareFp: bigint,
24
+ positionInterestIndexSnapshotFp: bigint
25
+ ): bigint {
26
+ const deltaFp = positiveDiff(
27
+ vaultAccInterestPerShareFp,
28
+ positionInterestIndexSnapshotFp
29
+ );
30
+
31
+ return (deposited * deltaFp) / INTEREST_FP_SCALE;
32
+ }
33
+
34
+ export async function depositToPosition(
35
+ params: BuildDepositToPositionParams
36
+ ) {
37
+ return buildDepositToPositionTransaction(params);
38
+ }
39
+
40
+ export async function withdrawFromPosition(
41
+ params: BuildWithdrawFromPositionParams
42
+ ) {
43
+ return buildWithdrawFromPositionTransaction(params);
44
+ }
45
+
46
+ export async function withdrawAllFromPosition(
47
+ rpc: KitRpc,
48
+ params: Omit<BuildWithdrawFromPositionParams, "amount"> & {
49
+ position: NonNullable<BuildWithdrawFromPositionParams["position"]>;
50
+ }
51
+ ): Promise<{ instructions: Awaited<ReturnType<typeof buildWithdrawFromPositionTransaction>>["instructions"]; amount: bigint }> {
52
+ const [position, vault] = await Promise.all([
53
+ fetchLenderPosition(rpc, params.position),
54
+ fetchVault(rpc, params.vault),
55
+ ]);
56
+
57
+ if (!position) {
58
+ throw new Error("Lender position not found. Provide position PDA or deposit first.");
59
+ }
60
+ if (!vault) {
61
+ throw new Error("Vault account not found.");
62
+ }
63
+
64
+ const unclaimedInterest = positiveDiff(
65
+ position.totalInterestEarned,
66
+ position.interestClaimed
67
+ );
68
+ const pendingInterest = calculatePendingInterest(
69
+ position.deposited,
70
+ vault.accInterestPerShareFp,
71
+ position.interestIndexSnapshotFp
72
+ );
73
+ const userMax = position.deposited + unclaimedInterest + pendingInterest;
74
+ const poolAvailable = positiveDiff(vault.totalLiquidity, vault.totalLoans);
75
+ const amount = userMax < poolAvailable ? userMax : poolAvailable;
76
+ if (amount <= 0n) {
77
+ throw new Error("No withdrawable balance available right now.");
78
+ }
79
+
80
+ const built = await buildWithdrawFromPositionTransaction({
81
+ ...params,
82
+ amount,
83
+ vaultMint: vault.mint,
84
+ });
85
+ return { instructions: built.instructions, amount };
86
+ }
87
+
88
+ export async function withdrawInterestFromPosition(
89
+ rpc: KitRpc,
90
+ params: Omit<BuildWithdrawFromPositionParams, "amount"> & {
91
+ position: NonNullable<BuildWithdrawFromPositionParams["position"]>;
92
+ }
93
+ ): Promise<{ instructions: Awaited<ReturnType<typeof buildWithdrawFromPositionTransaction>>["instructions"]; amount: bigint }> {
94
+ const [position, vault] = await Promise.all([
95
+ fetchLenderPosition(rpc, params.position),
96
+ fetchVault(rpc, params.vault),
97
+ ]);
98
+
99
+ if (!position) {
100
+ throw new Error("Lender position not found. Provide position PDA or deposit first.");
101
+ }
102
+ if (!vault) {
103
+ throw new Error("Vault account not found.");
104
+ }
105
+
106
+ const unclaimedInterest = positiveDiff(
107
+ position.totalInterestEarned,
108
+ position.interestClaimed
109
+ );
110
+ const pendingInterest = calculatePendingInterest(
111
+ position.deposited,
112
+ vault.accInterestPerShareFp,
113
+ position.interestIndexSnapshotFp
114
+ );
115
+ const totalClaimableInterest = unclaimedInterest + pendingInterest;
116
+ const poolAvailable = positiveDiff(vault.totalLiquidity, vault.totalLoans);
117
+ const amount =
118
+ totalClaimableInterest < poolAvailable
119
+ ? totalClaimableInterest
120
+ : poolAvailable;
121
+ if (amount <= 0n) {
122
+ throw new Error("No claimable interest available right now.");
123
+ }
124
+
125
+ const built = await buildWithdrawFromPositionTransaction({
126
+ ...params,
127
+ amount,
128
+ vaultMint: vault.mint,
129
+ });
130
+ return { instructions: built.instructions, amount };
131
+ }
132
+
133
+ export const omlpBuilders = {
134
+ buildDepositToPositionTransaction,
135
+ buildWithdrawFromPositionTransaction,
136
+ };
@@ -0,0 +1,315 @@
1
+ import type { Address, Instruction } from "@solana/kit";
2
+ import { fromLegacyTransactionInstruction } from "@solana/compat";
3
+ import { CrossbarClient } from "@switchboard-xyz/common";
4
+ import {
5
+ AnchorUtils,
6
+ ON_DEMAND_DEVNET_PID,
7
+ ON_DEMAND_MAINNET_PID,
8
+ Queue,
9
+ } from "@switchboard-xyz/on-demand";
10
+ import { Connection, type TransactionInstruction } from "@solana/web3.js";
11
+ import bs58 from "bs58";
12
+ import { toAddress } from "../client/program";
13
+ import type { AddressLike, BuiltTransaction, KitRpc } from "../client/types";
14
+ import { fetchMarketDataAccount } from "../accounts/fetchers";
15
+ import { invariant } from "../shared/errors";
16
+
17
+ const DEVNET_GENESIS_HASH = "EtWTRABZaYq6iMfeYKouRu166VU2xqa1wcaWoxPkrZBG";
18
+ const MAINNET_BETA_GENESIS_HASH = "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpKuc147dw2N9d";
19
+
20
+ export type SwitchboardNetwork = "devnet" | "mainnet";
21
+
22
+ export const SWITCHBOARD_DEFAULT_DEVNET_QUEUE =
23
+ "EYiAmGSdsQTuCw413V5BzaruWuCCSDgTPtBGvLkXHbe7";
24
+ export const SWITCHBOARD_DEFAULT_MAINNET_QUEUE =
25
+ "A43DyUGA7s8eXPxqEjJY6EBu1KKbNgfxF8h17VAHn13w";
26
+ export const SLOT_HASHES_SYSVAR_ADDRESS =
27
+ "SysvarS1otHashes111111111111111111111111111";
28
+ export const INSTRUCTIONS_SYSVAR_ADDRESS =
29
+ "Sysvar1nstructions1111111111111111111111111";
30
+
31
+ /**
32
+ * Default Switchboard on-demand queue for the cluster. Use as `switchboard_queue` on program
33
+ * instructions that verify the prepended quote ix — same defaults as Switchboard `Queue.loadDefault`,
34
+ * without threading `quote.queueAddress` from {@link buildSwitchboardQuoteInstruction}.
35
+ */
36
+ export function getDefaultSwitchboardQueueAddress(
37
+ network: SwitchboardNetwork
38
+ ): Address {
39
+ return toAddress(
40
+ network === "mainnet"
41
+ ? SWITCHBOARD_DEFAULT_MAINNET_QUEUE
42
+ : SWITCHBOARD_DEFAULT_DEVNET_QUEUE
43
+ );
44
+ }
45
+
46
+ const KNOWN_FEED_ID_TO_ACCOUNT: Record<SwitchboardNetwork, Record<string, string>> = {
47
+ devnet: {
48
+ "0x822512ee9add93518eca1c105a38422841a76c590db079eebb283deb2c14caa9":
49
+ "EneYGtye2n7jkSwGvQtwBaY6VBhP2mbizHD2y7hNkGFC",
50
+ "0x883ea8295f70ae506e894679d124196bb07064ea530cefd835b58c33a5ab6549":
51
+ "DHB2Ph8CK7PmR3xswqcmDkgQeucnwSZtfnMpnc7mQgkb",
52
+ },
53
+ mainnet: {
54
+ "0x822512ee9add93518eca1c105a38422841a76c590db079eebb283deb2c14caa9":
55
+ "4Hmd6PdjVA9auCoScE12iaBogfwS4ZXQ6VZoBeqanwWW",
56
+ "0x883ea8295f70ae506e894679d124196bb07064ea530cefd835b58c33a5ab6549":
57
+ "GckHmCwSyYvYDTJax4hhTzGMykV5JmgKDSaFkcnWPeU4",
58
+ },
59
+ };
60
+
61
+ export async function resolveSwitchboardFeedFromMarketData(
62
+ rpc: KitRpc,
63
+ marketData: AddressLike
64
+ ): Promise<Address> {
65
+ const account = await fetchMarketDataAccount(rpc, marketData);
66
+ invariant(!!account, "Market data account not found.");
67
+ const feedBytes = Uint8Array.from(
68
+ account.switchboardFeedId as unknown as Uint8Array
69
+ );
70
+ const feedIdHex = feedIdBytesToHex(feedBytes).toLowerCase();
71
+ const network = await inferSwitchboardNetwork(rpc);
72
+ const mappedFeedAccount = KNOWN_FEED_ID_TO_ACCOUNT[network][feedIdHex];
73
+ if (mappedFeedAccount) {
74
+ return toAddress(mappedFeedAccount);
75
+ }
76
+
77
+ // Backward compatibility for environments still storing feed account pubkey bytes.
78
+ return toAddress(bs58.encode(Array.from(feedBytes)));
79
+ }
80
+
81
+ export function feedIdBytesToHex(feedIdBytes: Uint8Array): string {
82
+ return `0x${Buffer.from(feedIdBytes).toString("hex")}`;
83
+ }
84
+
85
+ export async function resolveSwitchboardFeedIdFromMarketData(
86
+ rpc: KitRpc,
87
+ marketData: AddressLike
88
+ ): Promise<string> {
89
+ const account = await fetchMarketDataAccount(rpc, marketData);
90
+ invariant(!!account, "Market data account not found.");
91
+ return feedIdBytesToHex(
92
+ Uint8Array.from(account.switchboardFeedId as unknown as Uint8Array)
93
+ );
94
+ }
95
+
96
+ export interface BuildSwitchboardQuoteInstructionParams {
97
+ rpcEndpoint: string;
98
+ feedIdHex: string;
99
+ network?: SwitchboardNetwork;
100
+ crossbarUrl?: string;
101
+ numSignatures?: number;
102
+ instructionIdx?: number;
103
+ }
104
+
105
+ export interface SwitchboardQuoteInstructionResult {
106
+ instruction: Instruction<string>;
107
+ queueAddress: AddressLike;
108
+ }
109
+
110
+ export async function buildSwitchboardQuoteInstruction(
111
+ params: BuildSwitchboardQuoteInstructionParams
112
+ ): Promise<SwitchboardQuoteInstructionResult> {
113
+ const network = params.network ?? "devnet";
114
+ const normalizedFeedId = params.feedIdHex.startsWith("0x")
115
+ ? params.feedIdHex
116
+ : `0x${params.feedIdHex}`;
117
+
118
+ const connection = new Connection(params.rpcEndpoint, "processed");
119
+ const programId =
120
+ network === "mainnet" ? ON_DEMAND_MAINNET_PID : ON_DEMAND_DEVNET_PID;
121
+ const program = await AnchorUtils.loadProgramFromConnection(
122
+ connection,
123
+ undefined,
124
+ programId
125
+ );
126
+ const queue = await Queue.loadDefault(program);
127
+
128
+ const crossbar = params.crossbarUrl
129
+ ? new CrossbarClient(params.crossbarUrl)
130
+ : CrossbarClient.default();
131
+ const quoteIx = await queue.fetchQuoteIx(crossbar, [normalizedFeedId], {
132
+ numSignatures: params.numSignatures,
133
+ instructionIdx: params.instructionIdx ?? 0,
134
+ variableOverrides: {},
135
+ });
136
+
137
+ return {
138
+ instruction: fromLegacyTransactionInstruction(quoteIx),
139
+ queueAddress: toAddress(queue.pubkey.toBase58()),
140
+ };
141
+ }
142
+
143
+ /** Same as {@link buildSwitchboardQuoteInstruction} but returns a legacy web3.js instruction (for Anchor/web3.js callers). */
144
+ export async function buildSwitchboardQuoteWeb3JsInstruction(
145
+ params: BuildSwitchboardQuoteInstructionParams
146
+ ): Promise<{ instruction: TransactionInstruction; queueAddress: AddressLike }> {
147
+ const network = params.network ?? "devnet";
148
+ const normalizedFeedId = params.feedIdHex.startsWith("0x")
149
+ ? params.feedIdHex
150
+ : `0x${params.feedIdHex}`;
151
+
152
+ const connection = new Connection(params.rpcEndpoint, "processed");
153
+ const programId =
154
+ network === "mainnet" ? ON_DEMAND_MAINNET_PID : ON_DEMAND_DEVNET_PID;
155
+ const program = await AnchorUtils.loadProgramFromConnection(
156
+ connection,
157
+ undefined,
158
+ programId
159
+ );
160
+ const queue = await Queue.loadDefault(program);
161
+
162
+ const crossbar = params.crossbarUrl
163
+ ? new CrossbarClient(params.crossbarUrl)
164
+ : CrossbarClient.default();
165
+ const quoteIx = await queue.fetchQuoteIx(crossbar, [normalizedFeedId], {
166
+ numSignatures: params.numSignatures,
167
+ instructionIdx: params.instructionIdx ?? 0,
168
+ variableOverrides: {},
169
+ });
170
+
171
+ return {
172
+ instruction: quoteIx,
173
+ queueAddress: toAddress(queue.pubkey.toBase58()),
174
+ };
175
+ }
176
+
177
+ export interface SwitchboardPullFeedLike<TInstruction = unknown, TLookupTable = unknown> {
178
+ fetchUpdateIx(args: {
179
+ crossbarClient: unknown;
180
+ chain?: "solana";
181
+ network?: "devnet" | "mainnet";
182
+ }): Promise<[TInstruction | null, unknown, unknown, TLookupTable[]]>;
183
+ }
184
+
185
+ export interface BuildSwitchboardPullFeedUpdateParams<
186
+ TInstruction = unknown,
187
+ TLookupTable = unknown,
188
+ > {
189
+ pullFeed: SwitchboardPullFeedLike<TInstruction, TLookupTable>;
190
+ crossbarClient: unknown;
191
+ chain?: "solana";
192
+ network?: "devnet" | "mainnet";
193
+ }
194
+
195
+ export async function buildSwitchboardPullFeedUpdate<
196
+ TInstruction = unknown,
197
+ TLookupTable = unknown,
198
+ >(
199
+ params: BuildSwitchboardPullFeedUpdateParams<TInstruction, TLookupTable>
200
+ ): Promise<{ updateInstructions: TInstruction[]; lookupTables: TLookupTable[] }> {
201
+ const [pullIx, _responses, _success, luts] = await params.pullFeed.fetchUpdateIx({
202
+ crossbarClient: params.crossbarClient,
203
+ chain: params.chain ?? "solana",
204
+ network: params.network,
205
+ });
206
+
207
+ const updateInstructions: TInstruction[] = pullIx ? [pullIx] : [];
208
+ return {
209
+ updateInstructions,
210
+ lookupTables: luts ?? [],
211
+ };
212
+ }
213
+
214
+ export interface BuildSwitchboardCrankParams {
215
+ rpc: KitRpc;
216
+ payer: AddressLike;
217
+ switchboardFeed?: AddressLike;
218
+ marketData?: AddressLike;
219
+ network?: SwitchboardNetwork;
220
+ crossbarUrl?: string;
221
+ numSignatures?: number;
222
+ }
223
+
224
+ export interface SwitchboardCrankResult {
225
+ instructions: Instruction<string>[];
226
+ addressLookupTableAddresses: AddressLike[];
227
+ }
228
+
229
+ export async function inferSwitchboardNetwork(
230
+ rpc: KitRpc
231
+ ): Promise<SwitchboardNetwork> {
232
+ const genesisHash = await rpc.getGenesisHash().send();
233
+ if (genesisHash === DEVNET_GENESIS_HASH) {
234
+ return "devnet";
235
+ }
236
+ if (genesisHash === MAINNET_BETA_GENESIS_HASH) {
237
+ return "mainnet";
238
+ }
239
+ throw new Error(
240
+ `Unable to infer Switchboard network from genesis hash: ${genesisHash}`
241
+ );
242
+ }
243
+
244
+ export async function buildSwitchboardCrank(
245
+ params: BuildSwitchboardCrankParams
246
+ ): Promise<SwitchboardCrankResult> {
247
+ const resolvedFeed =
248
+ params.switchboardFeed ??
249
+ (params.marketData
250
+ ? await resolveSwitchboardFeedFromMarketData(params.rpc, params.marketData)
251
+ : undefined);
252
+
253
+ invariant(
254
+ !!resolvedFeed,
255
+ "switchboardFeed or marketData is required to build Switchboard crank instructions."
256
+ );
257
+
258
+ const network = params.network ?? (await inferSwitchboardNetwork(params.rpc));
259
+ const crossbar = params.crossbarUrl
260
+ ? new CrossbarClient(params.crossbarUrl)
261
+ : CrossbarClient.default();
262
+
263
+ try {
264
+ const updates = await crossbar.fetchSolanaUpdates(
265
+ network,
266
+ [toAddress(resolvedFeed)],
267
+ toAddress(params.payer),
268
+ params.numSignatures
269
+ );
270
+ const update = updates[0];
271
+
272
+ const instructions =
273
+ update?.pullIxns?.map((instruction) =>
274
+ fromLegacyTransactionInstruction(instruction)
275
+ ) ?? [];
276
+ const addressLookupTableAddresses = update?.lookupTables ?? [];
277
+
278
+ return {
279
+ instructions,
280
+ addressLookupTableAddresses,
281
+ };
282
+ } catch {
283
+ // Crossbar may return a non-array body or omit pullIxns; @switchboard-xyz/common then throws
284
+ // (e.g. undefined `.map`). Same outcome as disableSwitchboardCrank: submit the action without
285
+ // prepended pull-feed instructions.
286
+ return {
287
+ instructions: [],
288
+ addressLookupTableAddresses: [],
289
+ };
290
+ }
291
+ }
292
+
293
+ export function prependSwitchboardCrank(
294
+ crank: SwitchboardCrankResult,
295
+ action: BuiltTransaction
296
+ ): BuiltTransaction {
297
+ return {
298
+ instructions: [...crank.instructions, ...action.instructions],
299
+ addressLookupTableAddresses: [
300
+ ...(crank.addressLookupTableAddresses ?? []),
301
+ ...(action.addressLookupTableAddresses ?? []),
302
+ ],
303
+ };
304
+ }
305
+
306
+ /** Prepend a Switchboard quote ix at index 0 (required for verified_switchboard_quote_price on-chain). */
307
+ export function prependSwitchboardQuote(
308
+ quote: SwitchboardQuoteInstructionResult,
309
+ action: BuiltTransaction
310
+ ): BuiltTransaction {
311
+ return {
312
+ instructions: [quote.instruction, ...action.instructions],
313
+ addressLookupTableAddresses: [...(action.addressLookupTableAddresses ?? [])],
314
+ };
315
+ }
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@epicentral/sos-sdk",
3
+ "version": "0.9.0-beta",
4
+ "private": false,
5
+ "description": "Solana Option Standard SDK. The frontend-first SDK for Native Options Trading on Solana. Created by Epicentral Labs.",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "main": "./index.ts",
9
+ "types": "./index.ts",
10
+ "exports": {
11
+ ".": "./index.ts",
12
+ "./*": "./*"
13
+ },
14
+ "devDependencies": {
15
+ "dotenv-cli": "^8.0.0"
16
+ },
17
+ "dependencies": {
18
+ "@solana-program/address-lookup-table": "^0.11.0",
19
+ "@solana-program/compute-budget": "^0.13.0",
20
+ "@solana-program/system": "^0.11.0",
21
+ "@solana/compat": "^6.1.0",
22
+ "@solana/kit": "^6.1.0",
23
+ "@switchboard-xyz/common": "^5.7.0",
24
+ "@switchboard-xyz/on-demand": "^3.9.0",
25
+ "bs58": "^6.0.0",
26
+ "decimal.js": "^10.4.3"
27
+ },
28
+ "scripts": {
29
+ "typecheck": "tsc --project tsconfig.json --noEmit",
30
+ "publish-beta": "dotenv -e .env -- pnpm publish --access public --tag beta",
31
+ "publish-alpha": "dotenv -e .env -- pnpm publish --access public --tag alpha",
32
+ "deprecate": "dotenv -e .env -- pnpm exec npm deprecate"
33
+ }
34
+ }
@@ -0,0 +1,53 @@
1
+ import Decimal from "decimal.js";
2
+ import { SdkValidationError } from "./errors";
3
+
4
+ export function toBaseUnits(amount: Decimal.Value, decimals: number): bigint {
5
+ const scaled = new Decimal(amount).mul(new Decimal(10).pow(decimals));
6
+ return BigInt(scaled.floor().toFixed(0));
7
+ }
8
+
9
+ export function fromBaseUnits(amount: bigint | number, decimals: number): Decimal {
10
+ return new Decimal(amount.toString()).div(new Decimal(10).pow(decimals));
11
+ }
12
+
13
+ export function assertPositiveAmount(value: bigint | number, label: string): void {
14
+ const bigintValue = typeof value === "bigint" ? value : BigInt(value);
15
+ if (bigintValue <= 0n) {
16
+ throw new SdkValidationError(`${label} must be greater than zero.`);
17
+ }
18
+ }
19
+
20
+ export function assertNonNegativeAmount(value: bigint | number, label: string): void {
21
+ const bigintValue = typeof value === "bigint" ? value : BigInt(value);
22
+ if (bigintValue < 0n) {
23
+ throw new SdkValidationError(`${label} cannot be negative.`);
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Calculate required collateral for option position in token base units
29
+ * Matches on-chain formula: ((qty / 1_000_000) * 100 * strike) / spot * 10^decimals
30
+ *
31
+ * @param quantity - Option quantity in base units (1 contract = 1_000_000)
32
+ * @param strikePrice - Strike price in USD
33
+ * @param spotPrice - Current spot price of underlying in USD (from oracle)
34
+ * @param tokenDecimals - Number of decimals for the underlying token (e.g., 9 for SOL)
35
+ * @returns Required collateral in token base units
36
+ */
37
+ export function calculateRequiredCollateral(
38
+ quantity: bigint | number,
39
+ strikePrice: number,
40
+ spotPrice: number,
41
+ tokenDecimals: number
42
+ ): number {
43
+ // Convert base units to contract count
44
+ const contracts = Number(quantity) / 1_000_000;
45
+ const contractSize = 100; // 1 contract = 100 units of underlying
46
+
47
+ // USD value needed for collateral
48
+ const usdRequired = contracts * contractSize * strikePrice;
49
+
50
+ // Convert USD to token base units
51
+ const baseUnits = 10 ** tokenDecimals;
52
+ return (usdRequired / spotPrice) * baseUnits;
53
+ }
@@ -0,0 +1,57 @@
1
+ import type { Address } from "@solana/kit";
2
+ import { deriveAssociatedTokenAddress } from "../accounts/pdas";
3
+ import { toAddress } from "../client/program";
4
+ import type { AddressLike, KitRpc } from "../client/types";
5
+ import { NATIVE_MINT } from "../wsol/instructions";
6
+
7
+ /** SPL Token account data: amount field offset (u64 LE). */
8
+ const TOKEN_ACCOUNT_AMOUNT_OFFSET = 64;
9
+
10
+ function decodeTokenAccountAmount(data: Uint8Array): bigint {
11
+ if (data.length < TOKEN_ACCOUNT_AMOUNT_OFFSET + 8) return BigInt(0);
12
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
13
+ return view.getBigUint64(TOKEN_ACCOUNT_AMOUNT_OFFSET, true);
14
+ }
15
+
16
+ async function fetchTokenAccountBalance(rpc: KitRpc, ata: Address): Promise<bigint> {
17
+ const response = await rpc.getAccountInfo(ata, { encoding: "base64" }).send();
18
+ const accountInfo = response.value;
19
+ if (!accountInfo) return BigInt(0);
20
+ const [b64] = accountInfo.data;
21
+ if (!b64) return BigInt(0);
22
+ const binary = atob(b64);
23
+ const data = new Uint8Array(binary.length);
24
+ for (let i = 0; i < binary.length; i++) data[i] = binary.charCodeAt(i);
25
+ return decodeTokenAccountAmount(data);
26
+ }
27
+
28
+ /**
29
+ * Returns the SPL token balance for an owner and mint in base units (smallest units).
30
+ * Derives the associated token account; returns 0n if the ATA does not exist or has no data.
31
+ */
32
+ export async function getTokenBalance(
33
+ owner: AddressLike,
34
+ mint: AddressLike,
35
+ rpc: KitRpc
36
+ ): Promise<bigint> {
37
+ const ata = await deriveAssociatedTokenAddress(owner, mint);
38
+ return fetchTokenAccountBalance(rpc, ata);
39
+ }
40
+
41
+ /**
42
+ * Returns native SOL balance (lamports), wrapped SOL (WSOL) balance (base units), and total (native + wrapped).
43
+ * Use for SOL pools when the UI should show combined "total SOL".
44
+ */
45
+ export async function getCombinedSOLBalance(
46
+ owner: AddressLike,
47
+ rpc: KitRpc
48
+ ): Promise<{ native: bigint; wrapped: bigint; total: bigint }> {
49
+ const ownerAddress = toAddress(owner);
50
+ const [nativeResponse, wrappedBalance] = await Promise.all([
51
+ rpc.getBalance(ownerAddress).send(),
52
+ getTokenBalance(ownerAddress, NATIVE_MINT, rpc),
53
+ ]);
54
+ const native = nativeResponse.value;
55
+ const total = native + wrappedBalance;
56
+ return { native, wrapped: wrappedBalance, total };
57
+ }
@@ -0,0 +1,12 @@
1
+ export class SdkValidationError extends Error {
2
+ constructor(message: string) {
3
+ super(message);
4
+ this.name = "SdkValidationError";
5
+ }
6
+ }
7
+
8
+ export function invariant(condition: boolean, message: string): asserts condition {
9
+ if (!condition) {
10
+ throw new SdkValidationError(message);
11
+ }
12
+ }
@@ -0,0 +1,41 @@
1
+ import { AccountRole, type AccountMeta, type Instruction } from "@solana/kit";
2
+ import { toAddress } from "../client/program";
3
+ import type { AddressLike } from "../client/types";
4
+
5
+ export interface RemainingAccountInput {
6
+ address: AddressLike;
7
+ isWritable: boolean;
8
+ isSigner?: boolean;
9
+ }
10
+
11
+ function toAccountRole(input: RemainingAccountInput): AccountRole {
12
+ if (input.isWritable) {
13
+ return input.isSigner ? AccountRole.WRITABLE_SIGNER : AccountRole.WRITABLE;
14
+ }
15
+
16
+ return input.isSigner ? AccountRole.READONLY_SIGNER : AccountRole.READONLY;
17
+ }
18
+
19
+ export function appendRemainingAccounts(
20
+ instruction: Instruction<string>,
21
+ remainingAccounts: RemainingAccountInput[] | undefined
22
+ ): Instruction<string> {
23
+ if (!remainingAccounts?.length) {
24
+ return instruction;
25
+ }
26
+
27
+ const extras = remainingAccounts.map(
28
+ (account) =>
29
+ ({
30
+ address: toAddress(account.address),
31
+ role: toAccountRole(account),
32
+ }) as AccountMeta<string>
33
+ );
34
+
35
+ const existingAccounts = instruction.accounts ?? [];
36
+
37
+ return {
38
+ ...instruction,
39
+ accounts: [...existingAccounts, ...extras],
40
+ };
41
+ }
@@ -0,0 +1,27 @@
1
+ export interface TradeConfig {
2
+ slippageBps?: number;
3
+ computeUnitLimit?: number;
4
+ computeUnitPriceMicroLamports?: number;
5
+ }
6
+
7
+ let globalTradeConfig: TradeConfig = {};
8
+
9
+ export function setGlobalTradeConfig(config: TradeConfig): void {
10
+ globalTradeConfig = { ...config };
11
+ }
12
+
13
+ export function updateGlobalTradeConfig(config: Partial<TradeConfig>): void {
14
+ globalTradeConfig = { ...globalTradeConfig, ...config };
15
+ }
16
+
17
+ export function getGlobalTradeConfig(): TradeConfig {
18
+ return { ...globalTradeConfig };
19
+ }
20
+
21
+ export function resetGlobalTradeConfig(): void {
22
+ globalTradeConfig = {};
23
+ }
24
+
25
+ export function resolveTradeConfig(overrides?: Partial<TradeConfig>): TradeConfig {
26
+ return { ...globalTradeConfig, ...overrides };
27
+ }