@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,690 @@
1
+ import {
2
+ getBuyFromPoolInstructionAsync,
3
+ getCloseLongToPoolInstructionAsync,
4
+ } from "../generated/instructions";
5
+ import type { Instruction } from "@solana/kit";
6
+ import { toAddress } from "../client/program";
7
+ import type { AddressLike, BuiltTransaction, KitRpc } from "../client/types";
8
+ import { resolveOptionAccounts } from "../accounts/resolve-option";
9
+ import {
10
+ deriveAssociatedTokenAddress,
11
+ deriveBuyerPositionPda,
12
+ } from "../accounts/pdas";
13
+ import { assertNonNegativeAmount, assertPositiveAmount } from "../shared/amounts";
14
+ import { invariant } from "../shared/errors";
15
+ import {
16
+ appendRemainingAccounts,
17
+ type RemainingAccountInput,
18
+ } from "../shared/remaining-accounts";
19
+ import type { OptionType } from "../generated/types";
20
+ import {
21
+ getCloseAccountInstruction,
22
+ getCreateAssociatedTokenIdempotentInstructionWithAddress,
23
+ NATIVE_MINT,
24
+ } from "../wsol/instructions";
25
+ import { fetchMarketDataAccount, fetchOptionPool } from "../accounts/fetchers";
26
+ import {
27
+ getBuyFromPoolRemainingAccounts,
28
+ getCloseLongToPoolRemainingAccounts,
29
+ } from "./remaining-accounts";
30
+ import { applySlippageBps } from "./quotes";
31
+ import {
32
+ buildSwitchboardQuoteInstruction,
33
+ feedIdBytesToHex,
34
+ getDefaultSwitchboardQueueAddress,
35
+ inferSwitchboardNetwork,
36
+ prependSwitchboardQuote,
37
+ } from "../oracle/switchboard";
38
+ import { fetchWriterPositionsForPool } from "../accounts/list";
39
+ import { getGlobalTradeConfig } from "../shared/trade-config";
40
+
41
+ export interface BuildBuyFromPoolParams {
42
+ optionPool: AddressLike;
43
+ optionAccount: AddressLike;
44
+ longMint: AddressLike;
45
+ underlyingMint: AddressLike;
46
+ marketData: AddressLike;
47
+ /** Program `switchboard_queue` — use {@link getDefaultSwitchboardQueueAddress} for the cluster. */
48
+ switchboardQueue: AddressLike;
49
+ buyer: AddressLike;
50
+ buyerPaymentAccount: AddressLike;
51
+ escrowLongAccount: AddressLike;
52
+ premiumVault: AddressLike;
53
+ quantity: bigint | number;
54
+ premiumAmount: bigint | number;
55
+ buyerPosition?: AddressLike;
56
+ buyerOptionAccount?: AddressLike;
57
+ remainingAccounts?: RemainingAccountInput[];
58
+ }
59
+
60
+ export interface BuildCloseLongToPoolParams {
61
+ optionPool: AddressLike;
62
+ optionAccount: AddressLike;
63
+ collateralPool: AddressLike;
64
+ underlyingMint: AddressLike;
65
+ longMint: AddressLike;
66
+ escrowLongAccount: AddressLike;
67
+ premiumVault: AddressLike;
68
+ marketData: AddressLike;
69
+ switchboardQueue: AddressLike;
70
+ buyer: AddressLike;
71
+ buyerLongAccount: AddressLike;
72
+ buyerPayoutAccount: AddressLike;
73
+ collateralVault: AddressLike;
74
+ quantity: bigint | number;
75
+ minPayoutAmount: bigint | number;
76
+ buyerPosition?: AddressLike;
77
+ /**
78
+ * When true, appends an SPL CloseAccount to close the buyer's LONG token account after close_long_to_pool (reclaim rent).
79
+ * Set to true only when closing the entire position; for partial closes the LONG ATA still holds remaining tokens.
80
+ */
81
+ closeLongTokenAccount?: boolean;
82
+ /**
83
+ * When true and underlying is WSOL, appends an SPL CloseAccount to unwrap the payout ATA so the buyer receives native SOL.
84
+ * Ignored when underlyingMint is not WSOL.
85
+ */
86
+ unwrapPayoutSol?: boolean;
87
+ remainingAccounts?: RemainingAccountInput[];
88
+ }
89
+
90
+ export async function buildBuyFromPoolInstruction(
91
+ params: BuildBuyFromPoolParams
92
+ ): Promise<Instruction<string>> {
93
+ assertPositiveAmount(params.quantity, "quantity");
94
+ assertPositiveAmount(params.premiumAmount, "premiumAmount");
95
+
96
+ const kitInstruction = await getBuyFromPoolInstructionAsync({
97
+ optionPool: toAddress(params.optionPool),
98
+ optionAccount: toAddress(params.optionAccount),
99
+ longMint: toAddress(params.longMint),
100
+ underlyingMint: toAddress(params.underlyingMint),
101
+ marketData: toAddress(params.marketData),
102
+ switchboardQueue: toAddress(params.switchboardQueue),
103
+ buyer: toAddress(params.buyer) as any,
104
+ buyerPosition: params.buyerPosition ? toAddress(params.buyerPosition) : undefined,
105
+ buyerOptionAccount: params.buyerOptionAccount
106
+ ? toAddress(params.buyerOptionAccount)
107
+ : undefined,
108
+ buyerPaymentAccount: toAddress(params.buyerPaymentAccount),
109
+ escrowLongAccount: toAddress(params.escrowLongAccount),
110
+ premiumVault: toAddress(params.premiumVault),
111
+ quantity: params.quantity,
112
+ premiumAmount: params.premiumAmount,
113
+ });
114
+
115
+ return appendRemainingAccounts(kitInstruction, params.remainingAccounts);
116
+ }
117
+
118
+ /**
119
+ * Builds a buy-from-pool transaction. The returned transaction may include a
120
+ * leading create-ATA-idempotent instruction for the buyer's option account so
121
+ * first-time buyers succeed without a separate setup step.
122
+ */
123
+ export async function buildBuyFromPoolTransaction(
124
+ params: BuildBuyFromPoolParams
125
+ ): Promise<BuiltTransaction> {
126
+ const buyerOptionAccountAddress = params.buyerOptionAccount
127
+ ? toAddress(params.buyerOptionAccount)
128
+ : await deriveAssociatedTokenAddress(params.buyer, params.longMint);
129
+
130
+ const createAtaIx =
131
+ await getCreateAssociatedTokenIdempotentInstructionWithAddress(
132
+ params.buyer,
133
+ params.buyer,
134
+ params.longMint,
135
+ buyerOptionAccountAddress
136
+ );
137
+
138
+ const buyFromPoolIx = await buildBuyFromPoolInstruction(params);
139
+ return { instructions: [createAtaIx, buyFromPoolIx] };
140
+ }
141
+
142
+ export interface BuildBuyFromPoolTransactionWithDerivationParams {
143
+ underlyingAsset: AddressLike;
144
+ optionType: OptionType;
145
+ strikePrice: number;
146
+ expirationDate: bigint | number;
147
+ buyer: AddressLike;
148
+ buyerPaymentAccount: AddressLike;
149
+ /** When `disableSwitchboardCrank` is true, optional explicit queue (else devnet/mainnet default). */
150
+ switchboardQueue?: AddressLike;
151
+ quantity: bigint | number;
152
+ premiumAmount: bigint | number;
153
+ rpc: KitRpc;
154
+ /** Required unless `disableSwitchboardCrank` — HTTP RPC URL for Switchboard `fetchQuoteIx`. */
155
+ rpcEndpoint?: string;
156
+ programId?: AddressLike;
157
+ buyerPosition?: AddressLike;
158
+ buyerOptionAccount?: AddressLike;
159
+ remainingAccounts?: RemainingAccountInput[];
160
+ /** When false (default), prepends Switchboard quote ix and uses its queue in the program ix. */
161
+ disableSwitchboardCrank?: boolean;
162
+ switchboardCrossbarUrl?: string;
163
+ switchboardNumSignatures?: number;
164
+ /**
165
+ * 0-based index of the Switchboard quote ix in the **final** serialized transaction (after the wallet prepends compute-budget ixs).
166
+ * OPX `sendInstructions` prepends SetComputeUnitLimit + SetComputeUnitPrice by default → **2**.
167
+ * Use **1** if only SetComputeUnitLimit is prepended (e.g. `omitComputeUnitPriceInstruction`); **0** if no CU prepend.
168
+ */
169
+ switchboardQuoteInstructionIndex?: number;
170
+ }
171
+
172
+ const DEFAULT_MARKET_ORDER_SLIPPAGE_BUFFER_BASE_UNITS = 500_000n;
173
+
174
+ /**
175
+ * Bps added to the *cap* for `max_premium_amount` (stacked on user `slippageBps` / buffer paths).
176
+ * Primary alignment for 6042 is UI `quotedPremiumTotal` (see solana-opx pool buy + Switchboard spot vs UI spot).
177
+ * This is a small extra cushion for f64/rounding in `option-program` `pool.rs`.
178
+ */
179
+ const POOL_BUY_MAX_PREMIUM_ONCHAIN_PAD_BPS = 100;
180
+
181
+ interface MarketOrderBufferLikeParams {
182
+ slippageBufferBaseUnits?: bigint | number;
183
+ slippageBufferLamports?: bigint | number;
184
+ }
185
+
186
+ function normalizeMarketOrderSlippageBuffer(
187
+ params: MarketOrderBufferLikeParams,
188
+ underlyingMint: AddressLike
189
+ ): bigint {
190
+ const hasBaseUnits = params.slippageBufferBaseUnits !== undefined;
191
+ const hasLamports = params.slippageBufferLamports !== undefined;
192
+
193
+ invariant(
194
+ !(hasBaseUnits && hasLamports),
195
+ "Provide only one of slippageBufferBaseUnits or slippageBufferLamports."
196
+ );
197
+
198
+ if (hasBaseUnits) {
199
+ assertNonNegativeAmount(params.slippageBufferBaseUnits!, "slippageBufferBaseUnits");
200
+ return BigInt(params.slippageBufferBaseUnits!);
201
+ }
202
+
203
+ if (hasLamports) {
204
+ assertNonNegativeAmount(params.slippageBufferLamports!, "slippageBufferLamports");
205
+ invariant(
206
+ String(toAddress(underlyingMint)) === String(NATIVE_MINT),
207
+ "slippageBufferLamports is only supported for SOL/WSOL underlyings. Use slippageBufferBaseUnits for other assets."
208
+ );
209
+ return BigInt(params.slippageBufferLamports!);
210
+ }
211
+
212
+ return DEFAULT_MARKET_ORDER_SLIPPAGE_BUFFER_BASE_UNITS;
213
+ }
214
+
215
+ export async function buildBuyFromPoolTransactionWithDerivation(
216
+ params: BuildBuyFromPoolTransactionWithDerivationParams
217
+ ): Promise<BuiltTransaction> {
218
+ const resolved = await resolveOptionAccounts({
219
+ underlyingAsset: params.underlyingAsset,
220
+ optionType: params.optionType,
221
+ strikePrice: params.strikePrice,
222
+ expirationDate: params.expirationDate,
223
+ programId: params.programId,
224
+ rpc: params.rpc,
225
+ });
226
+
227
+ invariant(
228
+ !!resolved.escrowLongAccount &&
229
+ !!resolved.premiumVault &&
230
+ !!resolved.underlyingMint,
231
+ "Option pool must exist; ensure rpc is provided and pool is initialized."
232
+ );
233
+
234
+ const [buyerPosition, buyerOptionAccount] = await Promise.all([
235
+ params.buyerPosition
236
+ ? Promise.resolve(params.buyerPosition)
237
+ : deriveBuyerPositionPda(
238
+ params.buyer,
239
+ resolved.optionAccount,
240
+ params.programId
241
+ ).then(([addr]) => addr),
242
+ params.buyerOptionAccount
243
+ ? Promise.resolve(params.buyerOptionAccount)
244
+ : deriveAssociatedTokenAddress(params.buyer, resolved.longMint),
245
+ ]);
246
+
247
+ const marketDataAccount = await fetchMarketDataAccount(params.rpc, resolved.marketData);
248
+ invariant(
249
+ !!marketDataAccount,
250
+ "Market data account not found for resolved option market."
251
+ );
252
+ const feedIdHex = feedIdBytesToHex(
253
+ Uint8Array.from(marketDataAccount.switchboardFeedId as unknown as Uint8Array)
254
+ );
255
+ const network = await inferSwitchboardNetwork(params.rpc);
256
+
257
+ if (params.disableSwitchboardCrank === true) {
258
+ const switchboardQueue = params.switchboardQueue
259
+ ? toAddress(params.switchboardQueue)
260
+ : getDefaultSwitchboardQueueAddress(network);
261
+ return buildBuyFromPoolTransaction({
262
+ optionPool: resolved.optionPool,
263
+ optionAccount: resolved.optionAccount,
264
+ longMint: resolved.longMint,
265
+ underlyingMint: resolved.underlyingMint!,
266
+ marketData: resolved.marketData,
267
+ switchboardQueue,
268
+ buyer: params.buyer,
269
+ buyerPaymentAccount: params.buyerPaymentAccount,
270
+ escrowLongAccount: resolved.escrowLongAccount!,
271
+ premiumVault: resolved.premiumVault!,
272
+ quantity: params.quantity,
273
+ premiumAmount: params.premiumAmount,
274
+ buyerPosition,
275
+ buyerOptionAccount,
276
+ remainingAccounts: params.remainingAccounts,
277
+ });
278
+ }
279
+
280
+ invariant(
281
+ !!params.rpcEndpoint,
282
+ "rpcEndpoint is required to fetch Switchboard quote instructions (or set disableSwitchboardCrank)."
283
+ );
284
+
285
+ const quote = await buildSwitchboardQuoteInstruction({
286
+ rpcEndpoint: params.rpcEndpoint,
287
+ feedIdHex,
288
+ network,
289
+ crossbarUrl: params.switchboardCrossbarUrl,
290
+ numSignatures: params.switchboardNumSignatures,
291
+ instructionIdx: params.switchboardQuoteInstructionIndex ?? 2,
292
+ });
293
+
294
+ const actionTx = await buildBuyFromPoolTransaction({
295
+ optionPool: resolved.optionPool,
296
+ optionAccount: resolved.optionAccount,
297
+ longMint: resolved.longMint,
298
+ underlyingMint: resolved.underlyingMint!,
299
+ marketData: resolved.marketData,
300
+ switchboardQueue: getDefaultSwitchboardQueueAddress(network),
301
+ buyer: params.buyer,
302
+ buyerPaymentAccount: params.buyerPaymentAccount,
303
+ escrowLongAccount: resolved.escrowLongAccount!,
304
+ premiumVault: resolved.premiumVault!,
305
+ quantity: params.quantity,
306
+ premiumAmount: params.premiumAmount,
307
+ buyerPosition,
308
+ buyerOptionAccount,
309
+ remainingAccounts: params.remainingAccounts,
310
+ });
311
+
312
+ return prependSwitchboardQuote(quote, actionTx);
313
+ }
314
+
315
+ export interface BuildBuyFromPoolMarketOrderParams
316
+ extends Omit<
317
+ BuildBuyFromPoolTransactionWithDerivationParams,
318
+ "premiumAmount" | "remainingAccounts"
319
+ >,
320
+ MarketOrderBufferLikeParams {
321
+ quotedPremiumTotal: bigint | number;
322
+ }
323
+
324
+ /**
325
+ * High-level market-order buy builder.
326
+ * Refetches option pool and remaining writer-position accounts right before
327
+ * build and sets max premium = quotedPremiumTotal + slippage buffer.
328
+ */
329
+ export async function buildBuyFromPoolMarketOrderTransactionWithDerivation(
330
+ params: BuildBuyFromPoolMarketOrderParams
331
+ ): Promise<BuiltTransaction> {
332
+ assertPositiveAmount(params.quantity, "quantity");
333
+ assertPositiveAmount(params.quotedPremiumTotal, "quotedPremiumTotal");
334
+
335
+ const resolved = await resolveOptionAccounts({
336
+ underlyingAsset: params.underlyingAsset,
337
+ optionType: params.optionType,
338
+ strikePrice: params.strikePrice,
339
+ expirationDate: params.expirationDate,
340
+ programId: params.programId,
341
+ rpc: params.rpc,
342
+ });
343
+
344
+ const [refetchedPool, remainingAccounts, buyerPosition, buyerOptionAccount] =
345
+ await Promise.all([
346
+ fetchOptionPool(params.rpc, resolved.optionPool),
347
+ getBuyFromPoolRemainingAccounts(params.rpc, resolved.optionPool, params.programId),
348
+ params.buyerPosition
349
+ ? Promise.resolve(params.buyerPosition)
350
+ : deriveBuyerPositionPda(
351
+ params.buyer,
352
+ resolved.optionAccount,
353
+ params.programId
354
+ ).then(([addr]) => addr),
355
+ params.buyerOptionAccount
356
+ ? Promise.resolve(params.buyerOptionAccount)
357
+ : deriveAssociatedTokenAddress(params.buyer, resolved.longMint),
358
+ ]);
359
+
360
+ invariant(
361
+ !!refetchedPool,
362
+ "Option pool must exist; ensure rpc is provided and pool is initialized."
363
+ );
364
+
365
+ // Build-time coverage assertion: verify active writer liquidity >= requested quantity
366
+ // This catches data staleness between preflight and build
367
+ const quantity = BigInt(params.quantity);
368
+ const writerPositions = await fetchWriterPositionsForPool(
369
+ params.rpc,
370
+ resolved.optionPool,
371
+ params.programId
372
+ );
373
+ const activeUnsoldTotal = writerPositions
374
+ .filter((p) => !p.data.isSettled && !p.data.isLiquidated && p.data.unsoldQty > 0n)
375
+ .reduce((sum, p) => sum + p.data.unsoldQty, 0n);
376
+
377
+ invariant(
378
+ activeUnsoldTotal >= quantity,
379
+ `Insufficient active writer liquidity: available=${activeUnsoldTotal}, requested=${quantity}. ` +
380
+ `This may indicate data staleness - please refresh and retry.`
381
+ );
382
+
383
+ const globalTradeConfig = getGlobalTradeConfig();
384
+ const hasExplicitSlippageBuffer =
385
+ params.slippageBufferBaseUnits !== undefined ||
386
+ params.slippageBufferLamports !== undefined;
387
+ const quotePremium = BigInt(params.quotedPremiumTotal);
388
+ const onchainPad = (q: bigint) =>
389
+ (q * BigInt(POOL_BUY_MAX_PREMIUM_ONCHAIN_PAD_BPS)) / 10_000n;
390
+ const slippageBuffer = hasExplicitSlippageBuffer
391
+ ? normalizeMarketOrderSlippageBuffer(params, refetchedPool.underlyingMint) +
392
+ onchainPad(quotePremium)
393
+ : globalTradeConfig.slippageBps !== undefined
394
+ ? applySlippageBps(
395
+ quotePremium,
396
+ globalTradeConfig.slippageBps + POOL_BUY_MAX_PREMIUM_ONCHAIN_PAD_BPS
397
+ ) - quotePremium
398
+ : normalizeMarketOrderSlippageBuffer(params, refetchedPool.underlyingMint) +
399
+ onchainPad(quotePremium);
400
+ const maxPremiumAmount = quotePremium + slippageBuffer;
401
+ assertPositiveAmount(maxPremiumAmount, "maxPremiumAmount");
402
+
403
+ const marketDataAccount = await fetchMarketDataAccount(params.rpc, resolved.marketData);
404
+ invariant(
405
+ !!marketDataAccount,
406
+ "Market data account not found for resolved option market."
407
+ );
408
+ const feedIdHex = feedIdBytesToHex(
409
+ Uint8Array.from(marketDataAccount.switchboardFeedId as unknown as Uint8Array)
410
+ );
411
+ const network = await inferSwitchboardNetwork(params.rpc);
412
+
413
+ if (params.disableSwitchboardCrank === true) {
414
+ const switchboardQueue = params.switchboardQueue
415
+ ? toAddress(params.switchboardQueue)
416
+ : getDefaultSwitchboardQueueAddress(network);
417
+ return buildBuyFromPoolTransaction({
418
+ optionPool: resolved.optionPool,
419
+ optionAccount: resolved.optionAccount,
420
+ longMint: resolved.longMint,
421
+ underlyingMint: refetchedPool.underlyingMint,
422
+ marketData: resolved.marketData,
423
+ switchboardQueue,
424
+ buyer: params.buyer,
425
+ buyerPaymentAccount: params.buyerPaymentAccount,
426
+ escrowLongAccount: refetchedPool.escrowLongAccount,
427
+ premiumVault: refetchedPool.premiumVault,
428
+ quantity: params.quantity,
429
+ premiumAmount: maxPremiumAmount,
430
+ buyerPosition,
431
+ buyerOptionAccount,
432
+ remainingAccounts,
433
+ });
434
+ }
435
+
436
+ invariant(
437
+ !!params.rpcEndpoint,
438
+ "rpcEndpoint is required to fetch Switchboard quote instructions (or set disableSwitchboardCrank)."
439
+ );
440
+
441
+ const quote = await buildSwitchboardQuoteInstruction({
442
+ rpcEndpoint: params.rpcEndpoint,
443
+ feedIdHex,
444
+ network,
445
+ crossbarUrl: params.switchboardCrossbarUrl,
446
+ numSignatures: params.switchboardNumSignatures,
447
+ instructionIdx: params.switchboardQuoteInstructionIndex ?? 2,
448
+ });
449
+
450
+ const actionTx = await buildBuyFromPoolTransaction({
451
+ optionPool: resolved.optionPool,
452
+ optionAccount: resolved.optionAccount,
453
+ longMint: resolved.longMint,
454
+ underlyingMint: refetchedPool.underlyingMint,
455
+ marketData: resolved.marketData,
456
+ switchboardQueue: getDefaultSwitchboardQueueAddress(network),
457
+ buyer: params.buyer,
458
+ buyerPaymentAccount: params.buyerPaymentAccount,
459
+ escrowLongAccount: refetchedPool.escrowLongAccount,
460
+ premiumVault: refetchedPool.premiumVault,
461
+ quantity: params.quantity,
462
+ premiumAmount: maxPremiumAmount,
463
+ buyerPosition,
464
+ buyerOptionAccount,
465
+ remainingAccounts,
466
+ });
467
+
468
+ return prependSwitchboardQuote(quote, actionTx);
469
+ }
470
+
471
+ export async function buildCloseLongToPoolInstruction(
472
+ params: BuildCloseLongToPoolParams
473
+ ): Promise<Instruction<string>> {
474
+ assertPositiveAmount(params.quantity, "quantity");
475
+ invariant(
476
+ BigInt(params.minPayoutAmount) >= 0n,
477
+ "minPayoutAmount must be greater than or equal to zero."
478
+ );
479
+
480
+ const kitInstruction = await getCloseLongToPoolInstructionAsync({
481
+ optionPool: toAddress(params.optionPool),
482
+ optionAccount: toAddress(params.optionAccount),
483
+ collateralPool: toAddress(params.collateralPool),
484
+ underlyingMint: toAddress(params.underlyingMint),
485
+ longMint: toAddress(params.longMint),
486
+ escrowLongAccount: toAddress(params.escrowLongAccount),
487
+ premiumVault: toAddress(params.premiumVault),
488
+ marketData: toAddress(params.marketData),
489
+ switchboardQueue: toAddress(params.switchboardQueue),
490
+ buyer: toAddress(params.buyer) as any,
491
+ buyerLongAccount: toAddress(params.buyerLongAccount),
492
+ buyerPayoutAccount: toAddress(params.buyerPayoutAccount),
493
+ collateralVault: toAddress(params.collateralVault),
494
+ buyerPosition: params.buyerPosition ? toAddress(params.buyerPosition) : undefined,
495
+ quantity: params.quantity,
496
+ minPayoutAmount: params.minPayoutAmount,
497
+ });
498
+
499
+ return appendRemainingAccounts(kitInstruction, params.remainingAccounts);
500
+ }
501
+
502
+ export async function buildCloseLongToPoolTransaction(
503
+ params: BuildCloseLongToPoolParams
504
+ ): Promise<BuiltTransaction> {
505
+ const instruction = await buildCloseLongToPoolInstruction(params);
506
+ const instructions = [instruction];
507
+
508
+ if (params.closeLongTokenAccount === true) {
509
+ instructions.push(
510
+ getCloseAccountInstruction(
511
+ params.buyerLongAccount,
512
+ params.buyer,
513
+ params.buyer
514
+ )
515
+ );
516
+ }
517
+
518
+ const shouldUnwrapPayout =
519
+ params.unwrapPayoutSol === true &&
520
+ toAddress(params.underlyingMint) === toAddress(NATIVE_MINT);
521
+ if (shouldUnwrapPayout) {
522
+ instructions.push(
523
+ getCloseAccountInstruction(
524
+ params.buyerPayoutAccount,
525
+ params.buyer,
526
+ params.buyer
527
+ )
528
+ );
529
+ }
530
+
531
+ return { instructions };
532
+ }
533
+
534
+ export interface BuildCloseLongToPoolTransactionWithDerivationParams {
535
+ underlyingAsset: AddressLike;
536
+ optionType: OptionType;
537
+ strikePrice: number;
538
+ expirationDate: bigint | number;
539
+ buyer: AddressLike;
540
+ buyerLongAccount: AddressLike;
541
+ buyerPayoutAccount: AddressLike;
542
+ switchboardQueue?: AddressLike;
543
+ quantity: bigint | number;
544
+ minPayoutAmount: bigint | number;
545
+ rpc: KitRpc;
546
+ rpcEndpoint?: string;
547
+ programId?: AddressLike;
548
+ buyerPosition?: AddressLike;
549
+ /**
550
+ * When true (default), appends CloseAccount for the buyer's LONG token account after close_long_to_pool.
551
+ * Set to false when doing a partial close (LONG ATA still holds remaining tokens).
552
+ */
553
+ closeLongTokenAccount?: boolean;
554
+ /**
555
+ * When true (default for WSOL underlying), appends CloseAccount to unwrap payout WSOL ATA to native SOL.
556
+ * Only applies when option underlying is WSOL.
557
+ */
558
+ unwrapPayoutSol?: boolean;
559
+ remainingAccounts?: RemainingAccountInput[];
560
+ disableSwitchboardCrank?: boolean;
561
+ switchboardCrossbarUrl?: string;
562
+ switchboardNumSignatures?: number;
563
+ /**
564
+ * 0-based index of the Switchboard quote ix in the **final** tx after the wallet prepends compute-budget ixs.
565
+ * OPX `sendInstructions` prepends only `SetComputeUnitLimit` before program ixs → default **1** (quote after CU limit).
566
+ */
567
+ switchboardQuoteInstructionIndex?: number;
568
+ }
569
+
570
+ export async function buildCloseLongToPoolTransactionWithDerivation(
571
+ params: BuildCloseLongToPoolTransactionWithDerivationParams
572
+ ): Promise<BuiltTransaction> {
573
+ const resolved = await resolveOptionAccounts({
574
+ underlyingAsset: params.underlyingAsset,
575
+ optionType: params.optionType,
576
+ strikePrice: params.strikePrice,
577
+ expirationDate: params.expirationDate,
578
+ programId: params.programId,
579
+ rpc: params.rpc,
580
+ });
581
+
582
+ invariant(
583
+ !!resolved.escrowLongAccount &&
584
+ !!resolved.premiumVault &&
585
+ !!resolved.collateralVault &&
586
+ !!resolved.underlyingMint,
587
+ "Option pool and collateral pool must exist; ensure rpc is provided and pools are initialized."
588
+ );
589
+
590
+ const buyerPosition = params.buyerPosition
591
+ ? params.buyerPosition
592
+ : (await deriveBuyerPositionPda(
593
+ params.buyer,
594
+ resolved.optionAccount,
595
+ params.programId
596
+ ))[0];
597
+
598
+ const isWsolUnderlying =
599
+ toAddress(resolved.underlyingMint!) === toAddress(NATIVE_MINT);
600
+ const closeLongTokenAccount =
601
+ params.closeLongTokenAccount !== false;
602
+ const unwrapPayoutSol =
603
+ params.unwrapPayoutSol !== false && isWsolUnderlying;
604
+
605
+ // close_long_to_pool requires a complete set of active WriterPositions so the
606
+ // program can run the strict Hamilton completeness check. Auto-populate if
607
+ // the caller didn't supply remaining accounts explicitly.
608
+ const remainingAccounts =
609
+ params.remainingAccounts ??
610
+ (await getCloseLongToPoolRemainingAccounts(
611
+ params.rpc,
612
+ resolved.optionPool,
613
+ params.programId
614
+ ));
615
+
616
+ const marketDataAccount = await fetchMarketDataAccount(params.rpc, resolved.marketData);
617
+ invariant(
618
+ !!marketDataAccount,
619
+ "Market data account not found for resolved option market."
620
+ );
621
+ const feedIdHex = feedIdBytesToHex(
622
+ Uint8Array.from(marketDataAccount.switchboardFeedId as unknown as Uint8Array)
623
+ );
624
+ const network = await inferSwitchboardNetwork(params.rpc);
625
+
626
+ if (params.disableSwitchboardCrank === true) {
627
+ const switchboardQueue = params.switchboardQueue
628
+ ? toAddress(params.switchboardQueue)
629
+ : getDefaultSwitchboardQueueAddress(network);
630
+ return buildCloseLongToPoolTransaction({
631
+ optionPool: resolved.optionPool,
632
+ optionAccount: resolved.optionAccount,
633
+ collateralPool: resolved.collateralPool,
634
+ underlyingMint: resolved.underlyingMint!,
635
+ longMint: resolved.longMint,
636
+ escrowLongAccount: resolved.escrowLongAccount!,
637
+ premiumVault: resolved.premiumVault!,
638
+ marketData: resolved.marketData,
639
+ switchboardQueue,
640
+ buyer: params.buyer,
641
+ buyerLongAccount: params.buyerLongAccount,
642
+ buyerPayoutAccount: params.buyerPayoutAccount,
643
+ collateralVault: resolved.collateralVault!,
644
+ quantity: params.quantity,
645
+ minPayoutAmount: params.minPayoutAmount,
646
+ buyerPosition,
647
+ closeLongTokenAccount,
648
+ unwrapPayoutSol,
649
+ remainingAccounts,
650
+ });
651
+ }
652
+
653
+ invariant(
654
+ !!params.rpcEndpoint,
655
+ "rpcEndpoint is required to fetch Switchboard quote instructions (or set disableSwitchboardCrank)."
656
+ );
657
+
658
+ const quote = await buildSwitchboardQuoteInstruction({
659
+ rpcEndpoint: params.rpcEndpoint,
660
+ feedIdHex,
661
+ network,
662
+ crossbarUrl: params.switchboardCrossbarUrl,
663
+ numSignatures: params.switchboardNumSignatures,
664
+ instructionIdx: params.switchboardQuoteInstructionIndex ?? 1,
665
+ });
666
+
667
+ const actionTx = await buildCloseLongToPoolTransaction({
668
+ optionPool: resolved.optionPool,
669
+ optionAccount: resolved.optionAccount,
670
+ collateralPool: resolved.collateralPool,
671
+ underlyingMint: resolved.underlyingMint!,
672
+ longMint: resolved.longMint,
673
+ escrowLongAccount: resolved.escrowLongAccount!,
674
+ premiumVault: resolved.premiumVault!,
675
+ marketData: resolved.marketData,
676
+ switchboardQueue: getDefaultSwitchboardQueueAddress(network),
677
+ buyer: params.buyer,
678
+ buyerLongAccount: params.buyerLongAccount,
679
+ buyerPayoutAccount: params.buyerPayoutAccount,
680
+ collateralVault: resolved.collateralVault!,
681
+ quantity: params.quantity,
682
+ minPayoutAmount: params.minPayoutAmount,
683
+ buyerPosition,
684
+ closeLongTokenAccount,
685
+ unwrapPayoutSol,
686
+ remainingAccounts,
687
+ });
688
+
689
+ return prependSwitchboardQuote(quote, actionTx);
690
+ }