@epicentral/sos-sdk 0.3.0-alpha.2 → 0.4.0-alpha.2

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/README.md CHANGED
@@ -47,7 +47,9 @@ Additional modules:
47
47
 
48
48
  | Function | Description |
49
49
  |----------|-------------|
50
+ | `buildBuyFromPoolMarketOrderTransactionWithDerivation` | High-level market-order buy builder (refetches pool + remaining accounts, applies premium cap buffer). |
50
51
  | `buildBuyFromPoolTransactionWithDerivation` | Builds buy-from-pool transaction; resolves accounts from option identity. |
52
+ | `preflightBuyFromPoolMarketOrder` | Buy preflight helper for liquidity + remaining-account coverage checks. |
51
53
  | `buildCloseLongToPoolTransactionWithDerivation` | Builds close-long-to-pool transaction. |
52
54
  | `getBuyFromPoolRemainingAccounts` | Builds remaining_accounts for buy (writer positions, etc.). |
53
55
 
@@ -88,7 +90,11 @@ Borrow/repay for writers: use `buildOptionMintTransactionWithDerivation` (with v
88
90
 
89
91
  ## Unwind with Loan Repayment
90
92
 
91
- When a writer unwinds an unsold short that had borrowed from the OMLP pool, the program repays lenders from the collateral vault inside `unwind_writer_unsold` (burn LONG+SHORT, repay loans, then return collateral to writer) in one instruction.
93
+ When a writer unwinds an unsold short that had borrowed from the OMLP pool, the program now repays in this order inside `unwind_writer_unsold`:
94
+
95
+ 1. Collateral vault funds first.
96
+ 2. Writer fallback wallet source (`writerRepaymentAccount`) for any shortfall.
97
+ 3. If combined funds cannot cover principal + interest + protocol fees, unwind fails with a protocol custom error (not a generic SPL `0x1`).
92
98
 
93
99
  Use **`buildUnwindWriterUnsoldWithLoanRepayment`** so that:
94
100
 
@@ -97,24 +103,78 @@ Use **`buildUnwindWriterUnsoldWithLoanRepayment`** so that:
97
103
  3. `remaining_accounts` = **[PoolLoan₁, PoolLoan₂, ...]** only (capped at 20 loans per tx).
98
104
  4. One transaction burns, repays lenders from collateral vault, and returns collateral to the writer.
99
105
 
106
+ Use **`preflightUnwindWriterUnsold`** before building the transaction to get:
107
+
108
+ - Per-loan principal/interest/protocol-fee breakdown.
109
+ - Aggregate owed, collateral-vault available, wallet fallback required, and shortfall.
110
+ - `canRepayFully` so UI can block early with actionable messaging.
111
+
100
112
  If there are no active pool loans for that vault, the API still works and passes empty `remaining_accounts`.
101
113
 
102
114
  **Alternative (repay then unwind):** For writers with more than ~20 active loans, (1) build `repay_pool_loan_from_collateral` instructions first to reduce loans, then (2) unwind with the remaining loans.
103
115
 
104
116
  **Stuck loan (InsufficientEscrowBalance):** When standard repay fails with `InsufficientEscrowBalance` (escrow underfunded or drained), use `buildRepayPoolLoanFromWalletInstruction` or `buildRepayPoolLoanFromWalletTransaction`. Same accounts as `buildRepayPoolLoanInstruction`; maker pays full principal + interest + fees from their wallet.
105
117
 
118
+ ### Recommended Preflight + Unwind
119
+
120
+ ```ts
121
+ import {
122
+ preflightUnwindWriterUnsold,
123
+ buildUnwindWriterUnsoldWithLoanRepayment,
124
+ } from "@epicentral/sos-sdk";
125
+
126
+ const preflight = await preflightUnwindWriterUnsold({
127
+ underlyingAsset,
128
+ optionType,
129
+ strikePrice,
130
+ expirationDate,
131
+ writer,
132
+ unwindQty,
133
+ rpc,
134
+ });
135
+
136
+ if (!preflight.canRepayFully) {
137
+ throw new Error(`Unwind blocked. Shortfall: ${preflight.summary.shortfall.toString()}`);
138
+ }
139
+
140
+ const tx = await buildUnwindWriterUnsoldWithLoanRepayment({
141
+ underlyingAsset,
142
+ optionType,
143
+ strikePrice,
144
+ expirationDate,
145
+ writer,
146
+ unwindQty,
147
+ rpc,
148
+ });
149
+ ```
150
+
106
151
  ## Usage Examples
107
152
 
108
- ### Buy from pool (with derivation)
153
+ ### Buy From Pool (market order, high-level)
109
154
 
110
155
  ```ts
111
156
  import {
112
- buildBuyFromPoolTransactionWithDerivation,
113
- resolveOptionAccounts,
157
+ buildBuyFromPoolMarketOrderTransactionWithDerivation,
158
+ preflightBuyFromPoolMarketOrder,
114
159
  OptionType,
115
160
  } from "@epicentral/sos-sdk";
116
161
 
117
- const tx = await buildBuyFromPoolTransactionWithDerivation({
162
+ const preflight = await preflightBuyFromPoolMarketOrder({
163
+ underlyingAsset: "...",
164
+ optionType: OptionType.Call,
165
+ strikePrice: 100_000,
166
+ expirationDate: BigInt(1735689600),
167
+ quantity: 1_000_000,
168
+ rpc,
169
+ quotedPremiumTotal: 50_000,
170
+ slippageBufferBaseUnits: 500_000n,
171
+ });
172
+
173
+ if (!preflight.canBuy) {
174
+ throw new Error(preflight.reason ?? "Buy preflight failed");
175
+ }
176
+
177
+ const tx = await buildBuyFromPoolMarketOrderTransactionWithDerivation({
118
178
  underlyingAsset: "...",
119
179
  optionType: OptionType.Call,
120
180
  strikePrice: 100_000,
@@ -123,11 +183,30 @@ const tx = await buildBuyFromPoolTransactionWithDerivation({
123
183
  buyerPaymentAccount: buyerUsdcAta,
124
184
  priceUpdate: pythPriceFeed,
125
185
  quantity: 1_000_000,
126
- premiumAmount: 50_000,
186
+ quotedPremiumTotal: 50_000,
187
+ slippageBufferBaseUnits: 500_000n,
127
188
  rpc,
128
189
  });
129
190
  ```
130
191
 
192
+ ### Buy premium semantics (market orders)
193
+
194
+ - `premiumAmount` / `max_premium_amount` is a **max premium cap**, not an exact premium target.
195
+ - Program computes premium on-chain at execution time and fails with `SlippageToleranceExceeded` if computed premium exceeds the cap.
196
+ - High-level market builder computes cap as `quotedPremiumTotal + buffer`:
197
+ - Canonical: `slippageBufferBaseUnits`
198
+ - Convenience for SOL/WSOL: `slippageBufferLamports`
199
+ - Default buffer: `500_000` base units (0.0005 SOL lamports)
200
+
201
+ ### Buy liquidity errors (6041)
202
+
203
+ - `InsufficientPoolLiquidity` can happen when:
204
+ - `option_pool.total_available < quantity`, or
205
+ - remaining writer-position accounts cannot cover full quantity in the smallest-first fill loop.
206
+ - Recommended client flow:
207
+ 1. Run `preflightBuyFromPoolMarketOrder` for UX gating.
208
+ 2. Build via `buildBuyFromPoolMarketOrderTransactionWithDerivation` so pool + remaining accounts are refetched immediately before build.
209
+
131
210
  ### Unwind with loan repayment
132
211
 
133
212
  ```ts
@@ -4,7 +4,7 @@ import { PROGRAM_ID } from "./program";
4
4
  import type { KitRpc } from "./types";
5
5
 
6
6
  export const LOOKUP_TABLE_ADDRESSES: Record<"devnet" | "mainnet", Address | null> = {
7
- devnet: address("HsoWoDfW4yXVaXE31tEz167cjQn79TeXYxggSrXvRvri"),
7
+ devnet: address("B7kctbAQDEHT8gJcqRB3Bjkv4EkxMCZecy2H6EQNLQPo"),
8
8
  mainnet: null,
9
9
  };
10
10
 
@@ -184,6 +184,12 @@ export const OPTION_PROGRAM_ERROR__INVALID_ESCROW_MINT = 0x17c2; // 6082
184
184
  export const OPTION_PROGRAM_ERROR__ACCOUNT_FROZEN = 0x17c3; // 6083
185
185
  /** InvalidAccount: Invalid account - does not match expected account */
186
186
  export const OPTION_PROGRAM_ERROR__INVALID_ACCOUNT = 0x17c4; // 6084
187
+ /** UnwindRepayAccountsMissing: Unwind repayment accounts are required when active pool loans exist */
188
+ export const OPTION_PROGRAM_ERROR__UNWIND_REPAY_ACCOUNTS_MISSING = 0x17c5; // 6085
189
+ /** UnwindRepayWalletSourceMissing: Writer repayment account is required for unwind shortfall fallback */
190
+ export const OPTION_PROGRAM_ERROR__UNWIND_REPAY_WALLET_SOURCE_MISSING = 0x17c6; // 6086
191
+ /** UnwindRepayInsufficientTotalFunds: Insufficient total funds to fully repay unwind loans (principal + interest + protocol fees) */
192
+ export const OPTION_PROGRAM_ERROR__UNWIND_REPAY_INSUFFICIENT_TOTAL_FUNDS = 0x17c7; // 6087
187
193
 
188
194
  export type OptionProgramError =
189
195
  | typeof OPTION_PROGRAM_ERROR__ACCOUNT_FROZEN
@@ -270,6 +276,9 @@ export type OptionProgramError =
270
276
  | typeof OPTION_PROGRAM_ERROR__UNAUTHORIZED_OMLP
271
277
  | typeof OPTION_PROGRAM_ERROR__UNDERLYING_ASSET_MISMATCH
272
278
  | typeof OPTION_PROGRAM_ERROR__UNHEALTHY_POSITION
279
+ | typeof OPTION_PROGRAM_ERROR__UNWIND_REPAY_ACCOUNTS_MISSING
280
+ | typeof OPTION_PROGRAM_ERROR__UNWIND_REPAY_INSUFFICIENT_TOTAL_FUNDS
281
+ | typeof OPTION_PROGRAM_ERROR__UNWIND_REPAY_WALLET_SOURCE_MISSING
273
282
  | typeof OPTION_PROGRAM_ERROR__VALIDATION_REQUIRED;
274
283
 
275
284
  let optionProgramErrorMessages: Record<OptionProgramError, string> | undefined;
@@ -359,6 +368,9 @@ if (process.env.NODE_ENV !== "production") {
359
368
  [OPTION_PROGRAM_ERROR__UNAUTHORIZED_OMLP]: `Unauthorized to perform this OMLP action`,
360
369
  [OPTION_PROGRAM_ERROR__UNDERLYING_ASSET_MISMATCH]: `Underlying asset mismatch - market data or mint does not match option`,
361
370
  [OPTION_PROGRAM_ERROR__UNHEALTHY_POSITION]: `Health ratio below liquidation threshold`,
371
+ [OPTION_PROGRAM_ERROR__UNWIND_REPAY_ACCOUNTS_MISSING]: `Unwind repayment accounts are required when active pool loans exist`,
372
+ [OPTION_PROGRAM_ERROR__UNWIND_REPAY_INSUFFICIENT_TOTAL_FUNDS]: `Insufficient total funds to fully repay unwind loans (principal + interest + protocol fees)`,
373
+ [OPTION_PROGRAM_ERROR__UNWIND_REPAY_WALLET_SOURCE_MISSING]: `Writer repayment account is required for unwind shortfall fallback`,
362
374
  [OPTION_PROGRAM_ERROR__VALIDATION_REQUIRED]: `Must call option_validate before borrow/settlement`,
363
375
  };
364
376
  }
@@ -66,6 +66,7 @@ export type UnwindWriterUnsoldInstruction<
66
66
  TAccountOmlpVaultState extends string | AccountMeta<string> = string,
67
67
  TAccountOmlpVault extends string | AccountMeta<string> = string,
68
68
  TAccountFeeWallet extends string | AccountMeta<string> = string,
69
+ TAccountWriterRepaymentAccount extends string | AccountMeta<string> = string,
69
70
  TAccountWriter extends string | AccountMeta<string> = string,
70
71
  TAccountTokenProgram extends string | AccountMeta<string> =
71
72
  "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
@@ -115,6 +116,9 @@ export type UnwindWriterUnsoldInstruction<
115
116
  TAccountFeeWallet extends string
116
117
  ? WritableAccount<TAccountFeeWallet>
117
118
  : TAccountFeeWallet,
119
+ TAccountWriterRepaymentAccount extends string
120
+ ? WritableAccount<TAccountWriterRepaymentAccount>
121
+ : TAccountWriterRepaymentAccount,
118
122
  TAccountWriter extends string
119
123
  ? WritableSignerAccount<TAccountWriter> &
120
124
  AccountSignerMeta<TAccountWriter>
@@ -182,6 +186,7 @@ export type UnwindWriterUnsoldAsyncInput<
182
186
  TAccountOmlpVaultState extends string = string,
183
187
  TAccountOmlpVault extends string = string,
184
188
  TAccountFeeWallet extends string = string,
189
+ TAccountWriterRepaymentAccount extends string = string,
185
190
  TAccountWriter extends string = string,
186
191
  TAccountTokenProgram extends string = string,
187
192
  TAccountSystemProgram extends string = string,
@@ -212,6 +217,8 @@ export type UnwindWriterUnsoldAsyncInput<
212
217
  omlpVault?: Address<TAccountOmlpVault>;
213
218
  /** Protocol fee wallet (receives protocol fees) - optional */
214
219
  feeWallet?: Address<TAccountFeeWallet>;
220
+ /** Writer wallet source for shortfall fallback during unwind loan repayment - optional */
221
+ writerRepaymentAccount?: Address<TAccountWriterRepaymentAccount>;
215
222
  writer: TransactionSigner<TAccountWriter>;
216
223
  tokenProgram?: Address<TAccountTokenProgram>;
217
224
  systemProgram?: Address<TAccountSystemProgram>;
@@ -232,6 +239,7 @@ export async function getUnwindWriterUnsoldInstructionAsync<
232
239
  TAccountOmlpVaultState extends string,
233
240
  TAccountOmlpVault extends string,
234
241
  TAccountFeeWallet extends string,
242
+ TAccountWriterRepaymentAccount extends string,
235
243
  TAccountWriter extends string,
236
244
  TAccountTokenProgram extends string,
237
245
  TAccountSystemProgram extends string,
@@ -251,6 +259,7 @@ export async function getUnwindWriterUnsoldInstructionAsync<
251
259
  TAccountOmlpVaultState,
252
260
  TAccountOmlpVault,
253
261
  TAccountFeeWallet,
262
+ TAccountWriterRepaymentAccount,
254
263
  TAccountWriter,
255
264
  TAccountTokenProgram,
256
265
  TAccountSystemProgram
@@ -272,6 +281,7 @@ export async function getUnwindWriterUnsoldInstructionAsync<
272
281
  TAccountOmlpVaultState,
273
282
  TAccountOmlpVault,
274
283
  TAccountFeeWallet,
284
+ TAccountWriterRepaymentAccount,
275
285
  TAccountWriter,
276
286
  TAccountTokenProgram,
277
287
  TAccountSystemProgram
@@ -305,6 +315,10 @@ export async function getUnwindWriterUnsoldInstructionAsync<
305
315
  omlpVaultState: { value: input.omlpVaultState ?? null, isWritable: true },
306
316
  omlpVault: { value: input.omlpVault ?? null, isWritable: true },
307
317
  feeWallet: { value: input.feeWallet ?? null, isWritable: true },
318
+ writerRepaymentAccount: {
319
+ value: input.writerRepaymentAccount ?? null,
320
+ isWritable: true,
321
+ },
308
322
  writer: { value: input.writer ?? null, isWritable: true },
309
323
  tokenProgram: { value: input.tokenProgram ?? null, isWritable: false },
310
324
  systemProgram: { value: input.systemProgram ?? null, isWritable: false },
@@ -372,6 +386,7 @@ export async function getUnwindWriterUnsoldInstructionAsync<
372
386
  getAccountMeta(accounts.omlpVaultState),
373
387
  getAccountMeta(accounts.omlpVault),
374
388
  getAccountMeta(accounts.feeWallet),
389
+ getAccountMeta(accounts.writerRepaymentAccount),
375
390
  getAccountMeta(accounts.writer),
376
391
  getAccountMeta(accounts.tokenProgram),
377
392
  getAccountMeta(accounts.systemProgram),
@@ -395,6 +410,7 @@ export async function getUnwindWriterUnsoldInstructionAsync<
395
410
  TAccountOmlpVaultState,
396
411
  TAccountOmlpVault,
397
412
  TAccountFeeWallet,
413
+ TAccountWriterRepaymentAccount,
398
414
  TAccountWriter,
399
415
  TAccountTokenProgram,
400
416
  TAccountSystemProgram
@@ -415,6 +431,7 @@ export type UnwindWriterUnsoldInput<
415
431
  TAccountOmlpVaultState extends string = string,
416
432
  TAccountOmlpVault extends string = string,
417
433
  TAccountFeeWallet extends string = string,
434
+ TAccountWriterRepaymentAccount extends string = string,
418
435
  TAccountWriter extends string = string,
419
436
  TAccountTokenProgram extends string = string,
420
437
  TAccountSystemProgram extends string = string,
@@ -445,6 +462,8 @@ export type UnwindWriterUnsoldInput<
445
462
  omlpVault?: Address<TAccountOmlpVault>;
446
463
  /** Protocol fee wallet (receives protocol fees) - optional */
447
464
  feeWallet?: Address<TAccountFeeWallet>;
465
+ /** Writer wallet source for shortfall fallback during unwind loan repayment - optional */
466
+ writerRepaymentAccount?: Address<TAccountWriterRepaymentAccount>;
448
467
  writer: TransactionSigner<TAccountWriter>;
449
468
  tokenProgram?: Address<TAccountTokenProgram>;
450
469
  systemProgram?: Address<TAccountSystemProgram>;
@@ -465,6 +484,7 @@ export function getUnwindWriterUnsoldInstruction<
465
484
  TAccountOmlpVaultState extends string,
466
485
  TAccountOmlpVault extends string,
467
486
  TAccountFeeWallet extends string,
487
+ TAccountWriterRepaymentAccount extends string,
468
488
  TAccountWriter extends string,
469
489
  TAccountTokenProgram extends string,
470
490
  TAccountSystemProgram extends string,
@@ -484,6 +504,7 @@ export function getUnwindWriterUnsoldInstruction<
484
504
  TAccountOmlpVaultState,
485
505
  TAccountOmlpVault,
486
506
  TAccountFeeWallet,
507
+ TAccountWriterRepaymentAccount,
487
508
  TAccountWriter,
488
509
  TAccountTokenProgram,
489
510
  TAccountSystemProgram
@@ -504,6 +525,7 @@ export function getUnwindWriterUnsoldInstruction<
504
525
  TAccountOmlpVaultState,
505
526
  TAccountOmlpVault,
506
527
  TAccountFeeWallet,
528
+ TAccountWriterRepaymentAccount,
507
529
  TAccountWriter,
508
530
  TAccountTokenProgram,
509
531
  TAccountSystemProgram
@@ -536,6 +558,10 @@ export function getUnwindWriterUnsoldInstruction<
536
558
  omlpVaultState: { value: input.omlpVaultState ?? null, isWritable: true },
537
559
  omlpVault: { value: input.omlpVault ?? null, isWritable: true },
538
560
  feeWallet: { value: input.feeWallet ?? null, isWritable: true },
561
+ writerRepaymentAccount: {
562
+ value: input.writerRepaymentAccount ?? null,
563
+ isWritable: true,
564
+ },
539
565
  writer: { value: input.writer ?? null, isWritable: true },
540
566
  tokenProgram: { value: input.tokenProgram ?? null, isWritable: false },
541
567
  systemProgram: { value: input.systemProgram ?? null, isWritable: false },
@@ -574,6 +600,7 @@ export function getUnwindWriterUnsoldInstruction<
574
600
  getAccountMeta(accounts.omlpVaultState),
575
601
  getAccountMeta(accounts.omlpVault),
576
602
  getAccountMeta(accounts.feeWallet),
603
+ getAccountMeta(accounts.writerRepaymentAccount),
577
604
  getAccountMeta(accounts.writer),
578
605
  getAccountMeta(accounts.tokenProgram),
579
606
  getAccountMeta(accounts.systemProgram),
@@ -597,6 +624,7 @@ export function getUnwindWriterUnsoldInstruction<
597
624
  TAccountOmlpVaultState,
598
625
  TAccountOmlpVault,
599
626
  TAccountFeeWallet,
627
+ TAccountWriterRepaymentAccount,
600
628
  TAccountWriter,
601
629
  TAccountTokenProgram,
602
630
  TAccountSystemProgram
@@ -635,9 +663,11 @@ export type ParsedUnwindWriterUnsoldInstruction<
635
663
  omlpVault?: TAccountMetas[11] | undefined;
636
664
  /** Protocol fee wallet (receives protocol fees) - optional */
637
665
  feeWallet?: TAccountMetas[12] | undefined;
638
- writer: TAccountMetas[13];
639
- tokenProgram: TAccountMetas[14];
640
- systemProgram: TAccountMetas[15];
666
+ /** Writer wallet source for shortfall fallback during unwind loan repayment - optional */
667
+ writerRepaymentAccount?: TAccountMetas[13] | undefined;
668
+ writer: TAccountMetas[14];
669
+ tokenProgram: TAccountMetas[15];
670
+ systemProgram: TAccountMetas[16];
641
671
  };
642
672
  data: UnwindWriterUnsoldInstructionData;
643
673
  };
@@ -650,7 +680,7 @@ export function parseUnwindWriterUnsoldInstruction<
650
680
  InstructionWithAccounts<TAccountMetas> &
651
681
  InstructionWithData<ReadonlyUint8Array>,
652
682
  ): ParsedUnwindWriterUnsoldInstruction<TProgram, TAccountMetas> {
653
- if (instruction.accounts.length < 16) {
683
+ if (instruction.accounts.length < 17) {
654
684
  // TODO: Coded error.
655
685
  throw new Error("Not enough accounts");
656
686
  }
@@ -682,6 +712,7 @@ export function parseUnwindWriterUnsoldInstruction<
682
712
  omlpVaultState: getNextOptionalAccount(),
683
713
  omlpVault: getNextOptionalAccount(),
684
714
  feeWallet: getNextOptionalAccount(),
715
+ writerRepaymentAccount: getNextOptionalAccount(),
685
716
  writer: getNextAccount(),
686
717
  tokenProgram: getNextAccount(),
687
718
  systemProgram: getNextAccount(),
@@ -88,7 +88,7 @@ import {
88
88
  } from "../instructions";
89
89
 
90
90
  export const OPTION_PROGRAM_PROGRAM_ADDRESS =
91
- "AoLSsFTxnpa488AYf3RS2bwrv53zZY9aFYSFCAheek8q" as Address<"AoLSsFTxnpa488AYf3RS2bwrv53zZY9aFYSFCAheek8q">;
91
+ "818mtjAGCGMfFAmnuQummzyFweVw1odD7gcAT32iTjDz" as Address<"818mtjAGCGMfFAmnuQummzyFweVw1odD7gcAT32iTjDz">;
92
92
 
93
93
  export enum OptionProgramAccount {
94
94
  CollateralPool,
@@ -679,7 +679,7 @@ export function identifyOptionProgramInstruction(
679
679
  }
680
680
 
681
681
  export type ParsedOptionProgramInstruction<
682
- TProgram extends string = "AoLSsFTxnpa488AYf3RS2bwrv53zZY9aFYSFCAheek8q",
682
+ TProgram extends string = "818mtjAGCGMfFAmnuQummzyFweVw1odD7gcAT32iTjDz",
683
683
  > =
684
684
  | ({
685
685
  instructionType: OptionProgramInstruction.AcceptAdmin;
package/index.ts CHANGED
@@ -16,6 +16,7 @@ export * from "./shared/transactions";
16
16
 
17
17
  export * from "./long/builders";
18
18
  export * from "./long/exercise";
19
+ export * from "./long/preflight";
19
20
  export * from "./long/quotes";
20
21
  export {
21
22
  getBuyFromPoolRemainingAccounts,
@@ -25,6 +26,7 @@ export * from "./short/builders";
25
26
  export * from "./short/claim-theta";
26
27
  export * from "./short/close-option";
27
28
  export * from "./short/pool";
29
+ export * from "./short/preflight";
28
30
 
29
31
  export * from "./omlp/builders";
30
32
  export * from "./omlp/service";
package/long/builders.ts CHANGED
@@ -10,14 +10,16 @@ import {
10
10
  deriveAssociatedTokenAddress,
11
11
  deriveBuyerPositionPda,
12
12
  } from "../accounts/pdas";
13
- import { assertPositiveAmount } from "../shared/amounts";
13
+ import { assertNonNegativeAmount, assertPositiveAmount } from "../shared/amounts";
14
14
  import { invariant } from "../shared/errors";
15
15
  import {
16
16
  appendRemainingAccounts,
17
17
  type RemainingAccountInput,
18
18
  } from "../shared/remaining-accounts";
19
19
  import type { OptionType } from "../generated/types";
20
- import { getCreateAssociatedTokenIdempotentInstructionWithAddress } from "../wsol/instructions";
20
+ import { getCreateAssociatedTokenIdempotentInstructionWithAddress, NATIVE_MINT } from "../wsol/instructions";
21
+ import { fetchOptionPool } from "../accounts/fetchers";
22
+ import { getBuyFromPoolRemainingAccounts } from "./remaining-accounts";
21
23
 
22
24
  export interface BuildBuyFromPoolParams {
23
25
  optionPool: AddressLike;
@@ -127,6 +129,42 @@ export interface BuildBuyFromPoolTransactionWithDerivationParams {
127
129
  remainingAccounts?: RemainingAccountInput[];
128
130
  }
129
131
 
132
+ const DEFAULT_MARKET_ORDER_SLIPPAGE_BUFFER_BASE_UNITS = 500_000n;
133
+
134
+ interface MarketOrderBufferLikeParams {
135
+ slippageBufferBaseUnits?: bigint | number;
136
+ slippageBufferLamports?: bigint | number;
137
+ }
138
+
139
+ function normalizeMarketOrderSlippageBuffer(
140
+ params: MarketOrderBufferLikeParams,
141
+ underlyingMint: AddressLike
142
+ ): bigint {
143
+ const hasBaseUnits = params.slippageBufferBaseUnits !== undefined;
144
+ const hasLamports = params.slippageBufferLamports !== undefined;
145
+
146
+ invariant(
147
+ !(hasBaseUnits && hasLamports),
148
+ "Provide only one of slippageBufferBaseUnits or slippageBufferLamports."
149
+ );
150
+
151
+ if (hasBaseUnits) {
152
+ assertNonNegativeAmount(params.slippageBufferBaseUnits!, "slippageBufferBaseUnits");
153
+ return BigInt(params.slippageBufferBaseUnits!);
154
+ }
155
+
156
+ if (hasLamports) {
157
+ assertNonNegativeAmount(params.slippageBufferLamports!, "slippageBufferLamports");
158
+ invariant(
159
+ String(toAddress(underlyingMint)) === String(NATIVE_MINT),
160
+ "slippageBufferLamports is only supported for SOL/WSOL underlyings. Use slippageBufferBaseUnits for other assets."
161
+ );
162
+ return BigInt(params.slippageBufferLamports!);
163
+ }
164
+
165
+ return DEFAULT_MARKET_ORDER_SLIPPAGE_BUFFER_BASE_UNITS;
166
+ }
167
+
130
168
  export async function buildBuyFromPoolTransactionWithDerivation(
131
169
  params: BuildBuyFromPoolTransactionWithDerivationParams
132
170
  ): Promise<BuiltTransaction> {
@@ -178,6 +216,82 @@ export async function buildBuyFromPoolTransactionWithDerivation(
178
216
  });
179
217
  }
180
218
 
219
+ export interface BuildBuyFromPoolMarketOrderParams
220
+ extends Omit<
221
+ BuildBuyFromPoolTransactionWithDerivationParams,
222
+ "premiumAmount" | "remainingAccounts"
223
+ >,
224
+ MarketOrderBufferLikeParams {
225
+ quotedPremiumTotal: bigint | number;
226
+ }
227
+
228
+ /**
229
+ * High-level market-order buy builder.
230
+ * Refetches option pool and remaining writer-position accounts right before
231
+ * build and sets max premium = quotedPremiumTotal + slippage buffer.
232
+ */
233
+ export async function buildBuyFromPoolMarketOrderTransactionWithDerivation(
234
+ params: BuildBuyFromPoolMarketOrderParams
235
+ ): Promise<BuiltTransaction> {
236
+ assertPositiveAmount(params.quantity, "quantity");
237
+ assertPositiveAmount(params.quotedPremiumTotal, "quotedPremiumTotal");
238
+
239
+ const resolved = await resolveOptionAccounts({
240
+ underlyingAsset: params.underlyingAsset,
241
+ optionType: params.optionType,
242
+ strikePrice: params.strikePrice,
243
+ expirationDate: params.expirationDate,
244
+ programId: params.programId,
245
+ rpc: params.rpc,
246
+ });
247
+
248
+ const [refetchedPool, remainingAccounts, buyerPosition, buyerOptionAccount] =
249
+ await Promise.all([
250
+ fetchOptionPool(params.rpc, resolved.optionPool),
251
+ getBuyFromPoolRemainingAccounts(params.rpc, resolved.optionPool, params.programId),
252
+ params.buyerPosition
253
+ ? Promise.resolve(params.buyerPosition)
254
+ : deriveBuyerPositionPda(
255
+ params.buyer,
256
+ resolved.optionAccount,
257
+ params.programId
258
+ ).then(([addr]) => addr),
259
+ params.buyerOptionAccount
260
+ ? Promise.resolve(params.buyerOptionAccount)
261
+ : deriveAssociatedTokenAddress(params.buyer, resolved.longMint),
262
+ ]);
263
+
264
+ invariant(
265
+ !!refetchedPool,
266
+ "Option pool must exist; ensure rpc is provided and pool is initialized."
267
+ );
268
+
269
+ const slippageBuffer = normalizeMarketOrderSlippageBuffer(
270
+ params,
271
+ refetchedPool.underlyingMint
272
+ );
273
+ const maxPremiumAmount = BigInt(params.quotedPremiumTotal) + slippageBuffer;
274
+ assertPositiveAmount(maxPremiumAmount, "maxPremiumAmount");
275
+
276
+ return buildBuyFromPoolTransaction({
277
+ optionPool: resolved.optionPool,
278
+ optionAccount: resolved.optionAccount,
279
+ longMint: resolved.longMint,
280
+ underlyingMint: refetchedPool.underlyingMint,
281
+ marketData: resolved.marketData,
282
+ priceUpdate: params.priceUpdate,
283
+ buyer: params.buyer,
284
+ buyerPaymentAccount: params.buyerPaymentAccount,
285
+ escrowLongAccount: refetchedPool.escrowLongAccount,
286
+ premiumVault: refetchedPool.premiumVault,
287
+ quantity: params.quantity,
288
+ premiumAmount: maxPremiumAmount,
289
+ buyerPosition,
290
+ buyerOptionAccount,
291
+ remainingAccounts,
292
+ });
293
+ }
294
+
181
295
  export async function buildCloseLongToPoolInstruction(
182
296
  params: BuildCloseLongToPoolParams
183
297
  ): Promise<Instruction<string>> {
@@ -0,0 +1,114 @@
1
+ import type { AddressLike, KitRpc } from "../client/types";
2
+ import type { OptionType } from "../generated/types";
3
+ import { fetchWriterPositionsForPool } from "../accounts/list";
4
+ import { resolveOptionAccounts } from "../accounts/resolve-option";
5
+ import { fetchOptionPool } from "../accounts/fetchers";
6
+ import { assertNonNegativeAmount, assertPositiveAmount } from "../shared/amounts";
7
+ import { invariant } from "../shared/errors";
8
+
9
+ function toBigInt(value: bigint | number): bigint {
10
+ return typeof value === "bigint" ? value : BigInt(value);
11
+ }
12
+
13
+ export interface PreflightBuyFromPoolMarketOrderParams {
14
+ underlyingAsset: AddressLike;
15
+ optionType: OptionType;
16
+ strikePrice: number;
17
+ expirationDate: bigint | number;
18
+ quantity: bigint | number;
19
+ rpc: KitRpc;
20
+ programId?: AddressLike;
21
+ quotedPremiumTotal?: bigint | number;
22
+ slippageBufferBaseUnits?: bigint | number;
23
+ }
24
+
25
+ export interface BuyFromPoolMarketOrderPremiumSummary {
26
+ quotedPremiumTotal: bigint;
27
+ slippageBufferBaseUnits: bigint;
28
+ maxPremiumAmount: bigint;
29
+ }
30
+
31
+ export interface PreflightBuyFromPoolMarketOrderResult {
32
+ canBuy: boolean;
33
+ reason?: string;
34
+ poolTotalAvailable: bigint;
35
+ requestedQuantity: bigint;
36
+ remainingAccountsCount: number;
37
+ remainingUnsoldAggregate: bigint;
38
+ premium?: BuyFromPoolMarketOrderPremiumSummary;
39
+ }
40
+
41
+ export async function preflightBuyFromPoolMarketOrder(
42
+ params: PreflightBuyFromPoolMarketOrderParams
43
+ ): Promise<PreflightBuyFromPoolMarketOrderResult> {
44
+ assertPositiveAmount(params.quantity, "quantity");
45
+ const requestedQuantity = toBigInt(params.quantity);
46
+
47
+ const resolved = await resolveOptionAccounts({
48
+ underlyingAsset: params.underlyingAsset,
49
+ optionType: params.optionType,
50
+ strikePrice: params.strikePrice,
51
+ expirationDate: params.expirationDate,
52
+ programId: params.programId,
53
+ rpc: params.rpc,
54
+ });
55
+
56
+ const [optionPool, writerPositions] = await Promise.all([
57
+ fetchOptionPool(params.rpc, resolved.optionPool),
58
+ fetchWriterPositionsForPool(params.rpc, resolved.optionPool, params.programId),
59
+ ]);
60
+
61
+ invariant(
62
+ !!optionPool,
63
+ "Option pool must exist; ensure rpc is provided and pool is initialized."
64
+ );
65
+
66
+ const availableWriterPositions = writerPositions.filter(
67
+ ({ data }) => toBigInt(data.unsoldQty) > 0n
68
+ );
69
+ const remainingUnsoldAggregate = availableWriterPositions.reduce(
70
+ (acc, { data }) => acc + toBigInt(data.unsoldQty),
71
+ 0n
72
+ );
73
+ const poolTotalAvailable = toBigInt(optionPool.totalAvailable);
74
+
75
+ const hasPoolLiquidity = poolTotalAvailable >= requestedQuantity;
76
+ const hasWriterCoverage = remainingUnsoldAggregate >= requestedQuantity;
77
+
78
+ let reason: string | undefined;
79
+ if (!hasPoolLiquidity) {
80
+ reason = "Pool total_available is less than requested quantity.";
81
+ } else if (!hasWriterCoverage) {
82
+ reason =
83
+ "Remaining writer-position liquidity is insufficient to fully fill requested quantity.";
84
+ }
85
+
86
+ const result: PreflightBuyFromPoolMarketOrderResult = {
87
+ canBuy: hasPoolLiquidity && hasWriterCoverage,
88
+ reason,
89
+ poolTotalAvailable,
90
+ requestedQuantity,
91
+ remainingAccountsCount: availableWriterPositions.length,
92
+ remainingUnsoldAggregate,
93
+ };
94
+
95
+ if (params.quotedPremiumTotal !== undefined) {
96
+ assertPositiveAmount(params.quotedPremiumTotal, "quotedPremiumTotal");
97
+ if (params.slippageBufferBaseUnits !== undefined) {
98
+ assertNonNegativeAmount(params.slippageBufferBaseUnits, "slippageBufferBaseUnits");
99
+ }
100
+ const quotedPremiumTotal = toBigInt(params.quotedPremiumTotal);
101
+ const slippageBufferBaseUnits =
102
+ params.slippageBufferBaseUnits !== undefined
103
+ ? toBigInt(params.slippageBufferBaseUnits)
104
+ : 0n;
105
+
106
+ result.premium = {
107
+ quotedPremiumTotal,
108
+ slippageBufferBaseUnits,
109
+ maxPremiumAmount: quotedPremiumTotal + slippageBufferBaseUnits,
110
+ };
111
+ }
112
+
113
+ return result;
114
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@epicentral/sos-sdk",
3
- "version": "0.3.0-alpha.2",
3
+ "version": "0.4.0-alpha.2",
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",
package/short/builders.ts CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  getUnwindWriterUnsoldInstructionAsync,
6
6
  type OptionType,
7
7
  } from "../generated";
8
- import type { Instruction } from "@solana/kit";
8
+ import type { Instruction, TransactionSigner } from "@solana/kit";
9
9
  import { toAddress } from "../client/program";
10
10
  import type { AddressLike, BuiltTransaction, KitRpc } from "../client/types";
11
11
  import { fetchVault } from "../accounts/fetchers";
@@ -24,7 +24,12 @@ import {
24
24
  appendRemainingAccounts,
25
25
  type RemainingAccountInput,
26
26
  } from "../shared/remaining-accounts";
27
- import { getCreateAssociatedTokenIdempotentInstructionWithAddress } from "../wsol/instructions";
27
+ import {
28
+ NATIVE_MINT,
29
+ getCreateAssociatedTokenIdempotentInstructionWithAddress,
30
+ getWrapSOLInstructions,
31
+ } from "../wsol/instructions";
32
+ import { preflightUnwindWriterUnsold } from "./preflight";
28
33
 
29
34
  export interface BuildOptionMintParams {
30
35
  optionType: OptionType;
@@ -81,6 +86,7 @@ export interface BuildUnwindWriterUnsoldParams {
81
86
  omlpVaultState?: AddressLike;
82
87
  omlpVault?: AddressLike;
83
88
  feeWallet?: AddressLike;
89
+ writerRepaymentAccount?: AddressLike;
84
90
  remainingAccounts?: RemainingAccountInput[];
85
91
  }
86
92
 
@@ -304,23 +310,28 @@ export async function buildUnwindWriterUnsoldInstruction(
304
310
  ): Promise<Instruction<string>> {
305
311
  assertPositiveAmount(params.unwindQty, "unwindQty");
306
312
 
307
- const kitInstruction = await getUnwindWriterUnsoldInstructionAsync({
308
- optionPool: toAddress(params.optionPool),
309
- optionAccount: toAddress(params.optionAccount),
310
- collateralPool: params.collateralPool ? toAddress(params.collateralPool) : undefined,
311
- writerPosition: params.writerPosition ? toAddress(params.writerPosition) : undefined,
312
- longMint: toAddress(params.longMint),
313
- shortMint: toAddress(params.shortMint),
314
- escrowLongAccount: toAddress(params.escrowLongAccount),
315
- writerShortAccount: toAddress(params.writerShortAccount),
316
- collateralVault: toAddress(params.collateralVault),
317
- writerCollateralAccount: toAddress(params.writerCollateralAccount),
318
- omlpVaultState: params.omlpVaultState ? toAddress(params.omlpVaultState) : undefined,
319
- omlpVault: params.omlpVault ? toAddress(params.omlpVault) : undefined,
320
- feeWallet: params.feeWallet ? toAddress(params.feeWallet) : undefined,
321
- writer: toAddress(params.writer) as any,
322
- unwindQty: params.unwindQty,
323
- });
313
+ const kitInstruction = await getUnwindWriterUnsoldInstructionAsync(
314
+ {
315
+ optionPool: toAddress(params.optionPool),
316
+ optionAccount: toAddress(params.optionAccount),
317
+ collateralPool: params.collateralPool ? toAddress(params.collateralPool) : undefined,
318
+ writerPosition: params.writerPosition ? toAddress(params.writerPosition) : undefined,
319
+ longMint: toAddress(params.longMint),
320
+ shortMint: toAddress(params.shortMint),
321
+ escrowLongAccount: toAddress(params.escrowLongAccount),
322
+ writerShortAccount: toAddress(params.writerShortAccount),
323
+ collateralVault: toAddress(params.collateralVault),
324
+ writerCollateralAccount: toAddress(params.writerCollateralAccount),
325
+ omlpVaultState: params.omlpVaultState ? toAddress(params.omlpVaultState) : undefined,
326
+ omlpVault: params.omlpVault ? toAddress(params.omlpVault) : undefined,
327
+ feeWallet: params.feeWallet ? toAddress(params.feeWallet) : undefined,
328
+ writerRepaymentAccount: params.writerRepaymentAccount
329
+ ? toAddress(params.writerRepaymentAccount)
330
+ : undefined,
331
+ writer: toAddress(params.writer) as any,
332
+ unwindQty: params.unwindQty,
333
+ } as any
334
+ );
324
335
 
325
336
  return appendRemainingAccounts(kitInstruction, params.remainingAccounts);
326
337
  }
@@ -344,6 +355,7 @@ export interface BuildUnwindWriterUnsoldTransactionWithDerivationParams {
344
355
  omlpVaultState?: AddressLike;
345
356
  omlpVault?: AddressLike;
346
357
  feeWallet?: AddressLike;
358
+ writerRepaymentAccount?: AddressLike;
347
359
  /**
348
360
  * When repaying pool loans: [PoolLoan₁, PoolLoan₂, ...] (all writable).
349
361
  * omlpVaultState, omlpVault, feeWallet must also be passed.
@@ -363,6 +375,15 @@ export interface BuildUnwindWriterUnsoldWithLoanRepaymentParams {
363
375
  programId?: AddressLike;
364
376
  /** Override when pool fetch is not used; otherwise resolved from option pool. */
365
377
  underlyingMint?: AddressLike;
378
+ /** Optional explicit fallback source account. Defaults to writer ATA for underlying mint. */
379
+ writerRepaymentAccount?: AddressLike;
380
+ /**
381
+ * When true and underlying mint is WSOL, prepend wrap instructions for the
382
+ * detected wallet shortfall before unwind.
383
+ */
384
+ includeWrapForShortfall?: boolean;
385
+ /** Signer required when includeWrapForShortfall=true for WSOL paths. */
386
+ writerSigner?: TransactionSigner<string>;
366
387
  }
367
388
 
368
389
  /**
@@ -414,8 +435,27 @@ export async function buildUnwindWriterUnsoldWithLoanRepayment(
414
435
  const feeWallet = vault
415
436
  ? await deriveAssociatedTokenAddress(vault.feeWallet, underlyingMint)
416
437
  : undefined;
438
+ const writerRepaymentAccount =
439
+ params.writerRepaymentAccount ??
440
+ (await deriveAssociatedTokenAddress(params.writer, underlyingMint));
417
441
 
418
- return buildUnwindWriterUnsoldTransactionWithDerivation({
442
+ const preflight = await preflightUnwindWriterUnsold({
443
+ underlyingAsset: params.underlyingAsset,
444
+ optionType: params.optionType,
445
+ strikePrice: params.strikePrice,
446
+ expirationDate: params.expirationDate,
447
+ writer: params.writer,
448
+ unwindQty: params.unwindQty,
449
+ rpc: params.rpc,
450
+ programId: params.programId,
451
+ underlyingMint,
452
+ });
453
+ invariant(
454
+ preflight.canRepayFully,
455
+ `Unwind cannot fully repay loans: shortfall=${preflight.summary.shortfall}`
456
+ );
457
+
458
+ const unwindTx = await buildUnwindWriterUnsoldTransactionWithDerivation({
419
459
  underlyingAsset: params.underlyingAsset,
420
460
  optionType: params.optionType,
421
461
  strikePrice: params.strikePrice,
@@ -427,8 +467,30 @@ export async function buildUnwindWriterUnsoldWithLoanRepayment(
427
467
  omlpVaultState: vaultPda,
428
468
  omlpVault,
429
469
  feeWallet,
470
+ writerRepaymentAccount,
430
471
  remainingAccounts,
431
472
  });
473
+
474
+ if (
475
+ params.includeWrapForShortfall &&
476
+ toAddress(underlyingMint) === toAddress(NATIVE_MINT) &&
477
+ preflight.summary.walletFallbackRequired > 0n
478
+ ) {
479
+ invariant(
480
+ !!params.writerSigner,
481
+ "writerSigner is required when includeWrapForShortfall=true for WSOL shortfall top-up."
482
+ );
483
+ const wrapInstructions = await getWrapSOLInstructions({
484
+ payer: params.writerSigner,
485
+ owner: params.writer,
486
+ lamports: preflight.summary.walletFallbackRequired,
487
+ });
488
+ return {
489
+ instructions: [...wrapInstructions, ...unwindTx.instructions],
490
+ };
491
+ }
492
+
493
+ return unwindTx;
432
494
  }
433
495
 
434
496
  export async function buildUnwindWriterUnsoldTransactionWithDerivation(
@@ -471,6 +533,7 @@ export async function buildUnwindWriterUnsoldTransactionWithDerivation(
471
533
  omlpVaultState: params.omlpVaultState,
472
534
  omlpVault: params.omlpVault,
473
535
  feeWallet: params.feeWallet,
536
+ writerRepaymentAccount: params.writerRepaymentAccount,
474
537
  remainingAccounts: params.remainingAccounts,
475
538
  });
476
539
  }
@@ -0,0 +1,248 @@
1
+ import { toAddress } from "../client/program";
2
+ import type { AddressLike, KitRpc } from "../client/types";
3
+ import type { OptionType } from "../generated";
4
+ import { fetchCollateralPool, fetchVault, fetchWriterPosition } from "../accounts/fetchers";
5
+ import { fetchPoolLoansByMaker } from "../accounts/list";
6
+ import { deriveAssociatedTokenAddress, deriveVaultPda, deriveWriterPositionPda } from "../accounts/pdas";
7
+ import { resolveOptionAccounts } from "../accounts/resolve-option";
8
+ import { invariant } from "../shared/errors";
9
+
10
+ const TOKEN_ACCOUNT_AMOUNT_OFFSET = 64;
11
+ const BPS_DENOMINATOR = 10_000n;
12
+
13
+ function readTokenAccountAmount(data: Uint8Array): bigint {
14
+ if (data.length < TOKEN_ACCOUNT_AMOUNT_OFFSET + 8) return 0n;
15
+ const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
16
+ return view.getBigUint64(TOKEN_ACCOUNT_AMOUNT_OFFSET, true);
17
+ }
18
+
19
+ async function fetchTokenAmount(rpc: KitRpc, tokenAccount: AddressLike): Promise<bigint> {
20
+ const response = await rpc.getAccountInfo(toAddress(tokenAccount), { encoding: "base64" }).send();
21
+ const info = response.value;
22
+ if (!info) return 0n;
23
+ const [base64Data] = info.data;
24
+ if (!base64Data) return 0n;
25
+ const decoded = atob(base64Data);
26
+ const bytes = new Uint8Array(decoded.length);
27
+ for (let i = 0; i < decoded.length; i++) bytes[i] = decoded.charCodeAt(i);
28
+ return readTokenAccountAmount(bytes);
29
+ }
30
+
31
+ function toBigInt(value: bigint | number): bigint {
32
+ return typeof value === "bigint" ? value : BigInt(value);
33
+ }
34
+
35
+ export interface PreflightUnwindWriterUnsoldParams {
36
+ underlyingAsset: AddressLike;
37
+ optionType: OptionType;
38
+ strikePrice: number;
39
+ expirationDate: bigint | number;
40
+ writer: AddressLike;
41
+ unwindQty: bigint | number;
42
+ rpc: KitRpc;
43
+ programId?: AddressLike;
44
+ underlyingMint?: AddressLike;
45
+ writerRepaymentAccount?: AddressLike;
46
+ }
47
+
48
+ export interface UnwindLoanBreakdown {
49
+ loanAddress: string;
50
+ principal: bigint;
51
+ accruedInterest: bigint;
52
+ accruedProtocolFees: bigint;
53
+ newlyAccruedInterest: bigint;
54
+ newlyAccruedProtocolFees: bigint;
55
+ totalInterest: bigint;
56
+ totalProtocolFees: bigint;
57
+ totalOwed: bigint;
58
+ }
59
+
60
+ export interface UnwindPreflightSummary {
61
+ activeLoanCount: number;
62
+ totalPrincipal: bigint;
63
+ totalInterest: bigint;
64
+ totalProtocolFees: bigint;
65
+ totalOwed: bigint;
66
+ collateralVaultAvailable: bigint;
67
+ walletFallbackAvailable: bigint;
68
+ walletFallbackRequired: bigint;
69
+ shortfall: bigint;
70
+ }
71
+
72
+ export interface UnwindPreflightResult {
73
+ canUnwind: boolean;
74
+ canRepayFully: boolean;
75
+ reason?: string;
76
+ writerPositionAddress: string;
77
+ writerRepaymentAccount: string;
78
+ collateralVaultAddress: string;
79
+ loans: Array<UnwindLoanBreakdown>;
80
+ summary: UnwindPreflightSummary;
81
+ }
82
+
83
+ export async function preflightUnwindWriterUnsold(
84
+ params: PreflightUnwindWriterUnsoldParams
85
+ ): Promise<UnwindPreflightResult> {
86
+ const resolved = await resolveOptionAccounts({
87
+ underlyingAsset: params.underlyingAsset,
88
+ optionType: params.optionType,
89
+ strikePrice: params.strikePrice,
90
+ expirationDate: params.expirationDate,
91
+ programId: params.programId,
92
+ rpc: params.rpc,
93
+ });
94
+
95
+ invariant(
96
+ !!resolved.collateralVault && !!resolved.collateralPool && !!resolved.underlyingMint,
97
+ "Option/collateral pool state is required for unwind preflight."
98
+ );
99
+
100
+ const underlyingMint = params.underlyingMint ?? resolved.underlyingMint;
101
+ const [vaultPda] = await deriveVaultPda(underlyingMint, params.programId);
102
+ const vaultPdaAddress = toAddress(vaultPda);
103
+ const writerRepaymentAccount =
104
+ params.writerRepaymentAccount ??
105
+ (await deriveAssociatedTokenAddress(params.writer, underlyingMint));
106
+ const writerRepaymentAddress = toAddress(writerRepaymentAccount);
107
+ const [writerPositionAddress] = await deriveWriterPositionPda(
108
+ resolved.optionPool,
109
+ params.writer,
110
+ params.programId
111
+ );
112
+
113
+ const [writerPosition, collateralPool, vault, loans, currentSlot] = await Promise.all([
114
+ fetchWriterPosition(params.rpc, writerPositionAddress),
115
+ fetchCollateralPool(params.rpc, resolved.collateralPool),
116
+ fetchVault(params.rpc, vaultPda),
117
+ fetchPoolLoansByMaker(params.rpc, params.writer),
118
+ params.rpc.getSlot().send(),
119
+ ]);
120
+
121
+ invariant(!!writerPosition, "Writer position is required for unwind preflight.");
122
+ invariant(!!collateralPool, "Collateral pool is required for unwind preflight.");
123
+ invariant(!!vault, "Vault state is required for unwind preflight.");
124
+
125
+ const unwindQty = toBigInt(params.unwindQty);
126
+ const unsoldQty = toBigInt(writerPosition.unsoldQty);
127
+ if (unwindQty <= 0n) {
128
+ return {
129
+ canUnwind: false,
130
+ canRepayFully: false,
131
+ reason: "unwindQty must be > 0",
132
+ writerPositionAddress: String(writerPositionAddress),
133
+ writerRepaymentAccount: String(writerRepaymentAddress),
134
+ collateralVaultAddress: String(resolved.collateralVault),
135
+ loans: [],
136
+ summary: {
137
+ activeLoanCount: 0,
138
+ totalPrincipal: 0n,
139
+ totalInterest: 0n,
140
+ totalProtocolFees: 0n,
141
+ totalOwed: 0n,
142
+ collateralVaultAvailable: 0n,
143
+ walletFallbackAvailable: 0n,
144
+ walletFallbackRequired: 0n,
145
+ shortfall: 0n,
146
+ },
147
+ };
148
+ }
149
+ if (unwindQty > unsoldQty) {
150
+ return {
151
+ canUnwind: false,
152
+ canRepayFully: false,
153
+ reason: "unwindQty exceeds writer unsold quantity",
154
+ writerPositionAddress: String(writerPositionAddress),
155
+ writerRepaymentAccount: String(writerRepaymentAddress),
156
+ collateralVaultAddress: String(resolved.collateralVault),
157
+ loans: [],
158
+ summary: {
159
+ activeLoanCount: 0,
160
+ totalPrincipal: 0n,
161
+ totalInterest: 0n,
162
+ totalProtocolFees: 0n,
163
+ totalOwed: 0n,
164
+ collateralVaultAvailable: 0n,
165
+ walletFallbackAvailable: 0n,
166
+ walletFallbackRequired: 0n,
167
+ shortfall: 0n,
168
+ },
169
+ };
170
+ }
171
+
172
+ const slotNow = toBigInt(currentSlot);
173
+ const protocolFeeBps = BigInt(vault.protocolFeeBps);
174
+ const slotsPerYear = 63_072_000n;
175
+ const loanBreakdown: Array<UnwindLoanBreakdown> = [];
176
+
177
+ for (const loan of loans) {
178
+ if (toAddress(loan.data.vault) !== vaultPdaAddress || Number(loan.data.status) !== 1) continue;
179
+ const principal = toBigInt(loan.data.principal);
180
+ const accruedInterest = toBigInt(loan.data.accruedInterest);
181
+ const accruedProtocolFees = toBigInt(loan.data.accruedProtocolFees);
182
+ const rateBps = BigInt(loan.data.rateBps);
183
+ const lastUpdateSlot = toBigInt(loan.data.lastUpdateSlot);
184
+ const slotsElapsed = slotNow > lastUpdateSlot ? slotNow - lastUpdateSlot : 0n;
185
+ const newlyAccruedInterest =
186
+ slotsElapsed > 0n ? (principal * rateBps * slotsElapsed) / BPS_DENOMINATOR / slotsPerYear : 0n;
187
+ const newlyAccruedProtocolFees =
188
+ slotsElapsed > 0n
189
+ ? (principal * protocolFeeBps * slotsElapsed) / BPS_DENOMINATOR / slotsPerYear
190
+ : 0n;
191
+ const totalInterest = accruedInterest + newlyAccruedInterest;
192
+ const totalProtocolFees = accruedProtocolFees + newlyAccruedProtocolFees;
193
+ const totalOwed = principal + totalInterest + totalProtocolFees;
194
+
195
+ loanBreakdown.push({
196
+ loanAddress: String(loan.address),
197
+ principal,
198
+ accruedInterest,
199
+ accruedProtocolFees,
200
+ newlyAccruedInterest,
201
+ newlyAccruedProtocolFees,
202
+ totalInterest,
203
+ totalProtocolFees,
204
+ totalOwed,
205
+ });
206
+ }
207
+
208
+ const totals = loanBreakdown.reduce(
209
+ (acc, item) => ({
210
+ principal: acc.principal + item.principal,
211
+ interest: acc.interest + item.totalInterest,
212
+ fees: acc.fees + item.totalProtocolFees,
213
+ owed: acc.owed + item.totalOwed,
214
+ }),
215
+ { principal: 0n, interest: 0n, fees: 0n, owed: 0n }
216
+ );
217
+
218
+ const [collateralVaultAvailable, walletFallbackAvailable] = await Promise.all([
219
+ fetchTokenAmount(params.rpc, resolved.collateralVault!),
220
+ fetchTokenAmount(params.rpc, writerRepaymentAddress),
221
+ ]);
222
+
223
+ const walletFallbackRequired =
224
+ totals.owed > collateralVaultAvailable ? totals.owed - collateralVaultAvailable : 0n;
225
+ const totalAvailable = collateralVaultAvailable + walletFallbackAvailable;
226
+ const shortfall = totals.owed > totalAvailable ? totals.owed - totalAvailable : 0n;
227
+
228
+ return {
229
+ canUnwind: true,
230
+ canRepayFully: shortfall === 0n,
231
+ reason: shortfall === 0n ? undefined : "Insufficient combined collateral vault + writer fallback funds",
232
+ writerPositionAddress: String(writerPositionAddress),
233
+ writerRepaymentAccount: String(writerRepaymentAddress),
234
+ collateralVaultAddress: String(resolved.collateralVault),
235
+ loans: loanBreakdown,
236
+ summary: {
237
+ activeLoanCount: loanBreakdown.length,
238
+ totalPrincipal: totals.principal,
239
+ totalInterest: totals.interest,
240
+ totalProtocolFees: totals.fees,
241
+ totalOwed: totals.owed,
242
+ collateralVaultAvailable,
243
+ walletFallbackAvailable,
244
+ walletFallbackRequired,
245
+ shortfall,
246
+ },
247
+ };
248
+ }