@epicentral/sos-sdk 0.5.0-alpha.1 → 0.5.0-alpha.10

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
@@ -50,14 +50,14 @@ Additional modules:
50
50
  | `buildBuyFromPoolMarketOrderTransactionWithDerivation` | High-level market-order buy builder (refetches pool + remaining accounts, applies premium cap buffer). |
51
51
  | `buildBuyFromPoolTransactionWithDerivation` | Builds buy-from-pool transaction; resolves accounts from option identity. |
52
52
  | `preflightBuyFromPoolMarketOrder` | Buy preflight helper for liquidity + remaining-account coverage checks. |
53
- | `buildCloseLongToPoolTransactionWithDerivation` | Builds close-long-to-pool transaction. |
53
+ | `buildCloseLongToPoolTransactionWithDerivation` | Builds close-long-to-pool transaction; by default appends CloseAccount for buyer LONG ATA and unwraps WSOL payout when underlying is SOL. |
54
54
  | `getBuyFromPoolRemainingAccounts` | Builds remaining_accounts for buy (writer positions, etc.). |
55
55
 
56
56
  ### Short (Writer) Flows
57
57
 
58
58
  | Function | Description |
59
59
  |----------|-------------|
60
- | `buildOptionMintTransactionWithDerivation` | Builds option mint (write) transaction. Supports multi-collateral: use `collateralMint` to back positions with any supported asset (USDC, BTC, SOL, etc.). |
60
+ | `buildOptionMintTransactionWithDerivation` | Builds option mint (write) transaction. By default appends CloseAccount for the maker's LONG token account after mint (reclaim rent). Supports multi-collateral: use `collateralMint` to back positions with any supported asset (USDC, BTC, SOL, etc.). |
61
61
  | `buildUnwindWriterUnsoldTransactionWithDerivation` | Builds unwind unsold transaction. |
62
62
  | `buildUnwindWriterUnsoldWithLoanRepayment` | **Unwind + repay pool loans in one tx.** Use when closing unsold shorts that borrowed from OMLP. |
63
63
  | `buildSyncWriterPositionTransaction` | Syncs writer position with pool accumulators. |
@@ -73,12 +73,29 @@ Additional modules:
73
73
  | Function | Description |
74
74
  |----------|-------------|
75
75
  | `buildDepositToPositionTransaction` | Deposits liquidity to OMLP. |
76
- | `buildWithdrawFromPositionTransaction` | Withdraws liquidity. |
77
- | `withdrawAllFromPosition` | Withdraws full position (omlp/service). |
78
- | `withdrawInterestFromPosition` | Withdraws accrued interest only (omlp/service). |
76
+ | `buildWithdrawFromPositionTransaction` | Withdraws liquidity; supports optional same-tx WSOL unwrap via `unwrapSol` + `vaultMint`. |
77
+ | `withdrawAllFromPosition` | Withdraws full position (principal + proportional interest, including pending index accrual, capped by pool liquidity). |
78
+ | `withdrawInterestFromPosition` | Withdraws interest only (realized + pending index accrual, capped by pool liquidity). |
79
79
 
80
80
  Borrow/repay for writers: use `buildOptionMintTransactionWithDerivation` (with vault/poolLoan) and `buildRepayPoolLoanFromCollateralInstruction` or `buildUnwindWriterUnsoldWithLoanRepayment`.
81
81
 
82
+ ### Token account closing (option mint and close long)
83
+
84
+ - **Option mint (seller/writer):** After `option_mint`, all LONG tokens go to the pool escrow; the maker's LONG ATA is left with zero balance. The SDK **automatically appends an SPL CloseAccount instruction** (when `closeMakerLongAccount` is not set to `false`) so the maker reclaims rent. Use `buildOptionMintTransaction` or `buildOptionMintTransactionWithDerivation`; pass `closeMakerLongAccount: false` to skip closing the LONG ATA.
85
+ - **Close long (buyer):** When the buyer closes or exercises early via `close_long_to_pool`, LONG tokens are returned to the pool and payout is sent to the buyer's payout ATA. The SDK can:
86
+ - **Close the buyer's LONG token account** after the close instruction so rent is reclaimed. Use `closeLongTokenAccount: true` (default for `buildCloseLongToPoolTransactionWithDerivation`); set to `false` for **partial** closes (the LONG ATA still holds remaining tokens).
87
+ - **Unwrap WSOL payout** when the option underlying is SOL: append CloseAccount on the payout ATA so the buyer receives native SOL. Use `unwrapPayoutSol: true` (default for WSOL in the derivation builder); set to `false` to keep payout as WSOL.
88
+
89
+ ### OMLP withdraw behavior
90
+
91
+ - Interest is allocated proportionally via the vault interest-per-share index.
92
+ - On-chain `withdraw_from_position` syncs pending interest before transferring funds, so a lender withdrawal automatically includes their proportional earned interest when available.
93
+ - `withdrawAllFromPosition` and `withdrawInterestFromPosition` compute pending interest from `accInterestPerShareFp` and `interestIndexSnapshotFp`, then cap by `poolAvailable = totalLiquidity - totalLoans`.
94
+ - Optional WSOL unwrap in the same transaction:
95
+ - Set `unwrapSol: true` and provide `vaultMint`.
96
+ - If `vaultMint === NATIVE_MINT`, SDK appends a `CloseAccount` after withdraw to unwrap WSOL ATA to native SOL.
97
+ - For non-WSOL mints, the same builder remains token-agnostic and does not append unwrap instructions.
98
+
82
99
  ### WSOL / Token Helpers
83
100
 
84
101
  | Function | Description |
@@ -179,7 +196,8 @@ Use **`preflightUnwindWriterUnsold`** before building the transaction to get:
179
196
  - **Collateral return calculation** (proportional share, returnable amount).
180
197
  - Collateral-vault available, wallet fallback required, and shortfall.
181
198
  - **Top-up UX fields:** `collateralVaultShortfall`, `needsWalletTopUp`.
182
- - `canRepayFully` so UI can block early with actionable messaging.
199
+ - WSOL repay metadata: `solTopUpRequired`, `topUpRequiredForRepay`, `nativeSolAvailable`.
200
+ - `canRepayFully`, which now reflects effective repay solvency (including native SOL top-up capacity for WSOL paths).
183
201
 
184
202
  If there are no active pool loans for that vault, the API still works and passes empty `remaining_accounts`.
185
203
 
@@ -205,10 +223,6 @@ const preflight = await preflightUnwindWriterUnsold({
205
223
  rpc,
206
224
  });
207
225
 
208
- if (!preflight.canRepayFully) {
209
- throw new Error(`Unwind blocked. Shortfall: ${preflight.summary.shortfall.toString()}`);
210
- }
211
-
212
226
  const tx = await buildUnwindWriterUnsoldWithLoanRepayment({
213
227
  underlyingAsset,
214
228
  optionType,
@@ -217,9 +231,15 @@ const tx = await buildUnwindWriterUnsoldWithLoanRepayment({
217
231
  writer,
218
232
  unwindQty,
219
233
  rpc,
234
+ includeWrapForShortfall: true, // for WSOL paths, auto-wrap net top-up when needed
235
+ writerSigner: walletSigner, // required when wrapping is needed
220
236
  });
221
237
  ```
222
238
 
239
+ Notes:
240
+ - For WSOL underlyings, the builder wraps only the net required amount: `max(0, walletFallbackRequired - walletFallbackAvailable)`.
241
+ - If repayment is still insolvent after considering vault + fallback + native SOL top-up capacity, the builder throws an actionable insolvency error.
242
+
223
243
  ## Usage Examples
224
244
 
225
245
  ### Buy From Pool (market order, high-level)
@@ -283,6 +303,25 @@ The program uses distinct error codes for liquidity failures:
283
303
  1. Run `preflightBuyFromPoolMarketOrder` for UX gating (checks both pool and active writer liquidity).
284
304
  2. Build via `buildBuyFromPoolMarketOrderTransactionWithDerivation` – it refetches pool + remaining accounts and asserts active writer liquidity >= requested quantity before building.
285
305
 
306
+ ### Framework deserialization errors (`#3003`)
307
+
308
+ If simulation fails with `custom program error: #3003`, this usually means account deserialization failed before business logic (`60xx`) ran.
309
+
310
+ Check these first:
311
+
312
+ - `buyer_position` account shape/size (`146` bytes expected).
313
+ - `market_data` account shape/size (`128` bytes expected).
314
+ - `priceUpdate` is a valid Pyth Receiver `PriceUpdateV2` account.
315
+ - Account list/order matches the generated instruction layout.
316
+
317
+ This is different from liquidity failures (`6042/6043`) and should be debugged as an account wiring/layout issue.
318
+
319
+ ### Oracle inputs (asset-agnostic)
320
+
321
+ - Keep oracle handling universal across assets.
322
+ - Provide `priceUpdate` for the selected underlying and allow program-side validation against `market_data.pyth_feed_id`.
323
+ - Avoid hardcoding a single feed/account address in shared SDK integration flows.
324
+
286
325
  ### Unwind with loan repayment
287
326
 
288
327
  ```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("B7kctbAQDEHT8gJcqRB3Bjkv4EkxMCZecy2H6EQNLQPo"),
7
+ devnet: address("7aGuGaRRdDA4KtsaWSCvFvAexv4sg8A8kkFKXuvsB58v"),
8
8
  mainnet: null,
9
9
  };
10
10
 
@@ -88,7 +88,7 @@ import {
88
88
  } from "../instructions";
89
89
 
90
90
  export const OPTION_PROGRAM_PROGRAM_ADDRESS =
91
- "BUszj34jkTxGik2AuVC4oQ3oKNSbkUaZsyNT3DSV8Qgm" as Address<"BUszj34jkTxGik2AuVC4oQ3oKNSbkUaZsyNT3DSV8Qgm">;
91
+ "Box8aCPTes6zAdgAh2e25Wo64PbFfoi9T4ToiZsfetVo" as Address<"Box8aCPTes6zAdgAh2e25Wo64PbFfoi9T4ToiZsfetVo">;
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 = "BUszj34jkTxGik2AuVC4oQ3oKNSbkUaZsyNT3DSV8Qgm",
682
+ TProgram extends string = "Box8aCPTes6zAdgAh2e25Wo64PbFfoi9T4ToiZsfetVo",
683
683
  > =
684
684
  | ({
685
685
  instructionType: OptionProgramInstruction.AcceptAdmin;
package/long/builders.ts CHANGED
@@ -17,7 +17,11 @@ import {
17
17
  type RemainingAccountInput,
18
18
  } from "../shared/remaining-accounts";
19
19
  import type { OptionType } from "../generated/types";
20
- import { getCreateAssociatedTokenIdempotentInstructionWithAddress, NATIVE_MINT } from "../wsol/instructions";
20
+ import {
21
+ getCloseAccountInstruction,
22
+ getCreateAssociatedTokenIdempotentInstructionWithAddress,
23
+ NATIVE_MINT,
24
+ } from "../wsol/instructions";
21
25
  import { fetchOptionPool } from "../accounts/fetchers";
22
26
  import { getBuyFromPoolRemainingAccounts } from "./remaining-accounts";
23
27
  import { fetchWriterPositionsForPool } from "../accounts/list";
@@ -58,6 +62,16 @@ export interface BuildCloseLongToPoolParams {
58
62
  minPayoutAmount: bigint | number;
59
63
  buyerPosition?: AddressLike;
60
64
  omlpVault?: AddressLike;
65
+ /**
66
+ * When true, appends an SPL CloseAccount to close the buyer's LONG token account after close_long_to_pool (reclaim rent).
67
+ * Set to true only when closing the entire position; for partial closes the LONG ATA still holds remaining tokens.
68
+ */
69
+ closeLongTokenAccount?: boolean;
70
+ /**
71
+ * When true and underlying is WSOL, appends an SPL CloseAccount to unwrap the payout ATA so the buyer receives native SOL.
72
+ * Ignored when underlyingMint is not WSOL.
73
+ */
74
+ unwrapPayoutSol?: boolean;
61
75
  remainingAccounts?: RemainingAccountInput[];
62
76
  }
63
77
 
@@ -347,7 +361,32 @@ export async function buildCloseLongToPoolTransaction(
347
361
  params: BuildCloseLongToPoolParams
348
362
  ): Promise<BuiltTransaction> {
349
363
  const instruction = await buildCloseLongToPoolInstruction(params);
350
- return { instructions: [instruction] };
364
+ const instructions = [instruction];
365
+
366
+ if (params.closeLongTokenAccount === true) {
367
+ instructions.push(
368
+ getCloseAccountInstruction(
369
+ params.buyerLongAccount,
370
+ params.buyer,
371
+ params.buyer
372
+ )
373
+ );
374
+ }
375
+
376
+ const shouldUnwrapPayout =
377
+ params.unwrapPayoutSol === true &&
378
+ toAddress(params.underlyingMint) === toAddress(NATIVE_MINT);
379
+ if (shouldUnwrapPayout) {
380
+ instructions.push(
381
+ getCloseAccountInstruction(
382
+ params.buyerPayoutAccount,
383
+ params.buyer,
384
+ params.buyer
385
+ )
386
+ );
387
+ }
388
+
389
+ return { instructions };
351
390
  }
352
391
 
353
392
  export interface BuildCloseLongToPoolTransactionWithDerivationParams {
@@ -365,6 +404,16 @@ export interface BuildCloseLongToPoolTransactionWithDerivationParams {
365
404
  programId?: AddressLike;
366
405
  buyerPosition?: AddressLike;
367
406
  omlpVault?: AddressLike;
407
+ /**
408
+ * When true (default), appends CloseAccount for the buyer's LONG token account after close_long_to_pool.
409
+ * Set to false when doing a partial close (LONG ATA still holds remaining tokens).
410
+ */
411
+ closeLongTokenAccount?: boolean;
412
+ /**
413
+ * When true (default for WSOL underlying), appends CloseAccount to unwrap payout WSOL ATA to native SOL.
414
+ * Only applies when option underlying is WSOL.
415
+ */
416
+ unwrapPayoutSol?: boolean;
368
417
  remainingAccounts?: RemainingAccountInput[];
369
418
  }
370
419
 
@@ -396,6 +445,13 @@ export async function buildCloseLongToPoolTransactionWithDerivation(
396
445
  params.programId
397
446
  ))[0];
398
447
 
448
+ const isWsolUnderlying =
449
+ toAddress(resolved.underlyingMint!) === toAddress(NATIVE_MINT);
450
+ const closeLongTokenAccount =
451
+ params.closeLongTokenAccount !== false;
452
+ const unwrapPayoutSol =
453
+ params.unwrapPayoutSol !== false && isWsolUnderlying;
454
+
399
455
  return buildCloseLongToPoolTransaction({
400
456
  optionPool: resolved.optionPool,
401
457
  optionAccount: resolved.optionAccount,
@@ -414,6 +470,8 @@ export async function buildCloseLongToPoolTransactionWithDerivation(
414
470
  minPayoutAmount: params.minPayoutAmount,
415
471
  buyerPosition,
416
472
  omlpVault: params.omlpVault,
473
+ closeLongTokenAccount,
474
+ unwrapPayoutSol,
417
475
  remainingAccounts: params.remainingAccounts,
418
476
  });
419
477
  }
package/omlp/builders.ts CHANGED
@@ -6,6 +6,7 @@ import type { Instruction } from "@solana/kit";
6
6
  import { toAddress } from "../client/program";
7
7
  import type { AddressLike, BuiltTransaction } from "../client/types";
8
8
  import { assertPositiveAmount } from "../shared/amounts";
9
+ import { getCloseAccountInstruction, NATIVE_MINT } from "../wsol/instructions";
9
10
 
10
11
  export interface BuildDepositToPositionParams {
11
12
  vault: AddressLike;
@@ -23,6 +24,8 @@ export interface BuildWithdrawFromPositionParams {
23
24
  lender: AddressLike;
24
25
  amount: bigint | number;
25
26
  position?: AddressLike;
27
+ unwrapSol?: boolean;
28
+ vaultMint?: AddressLike;
26
29
  }
27
30
 
28
31
  export async function buildDepositToPositionInstruction(
@@ -69,6 +72,23 @@ export async function buildWithdrawFromPositionInstruction(
69
72
  export async function buildWithdrawFromPositionTransaction(
70
73
  params: BuildWithdrawFromPositionParams
71
74
  ): Promise<BuiltTransaction> {
72
- const instruction = await buildWithdrawFromPositionInstruction(params);
73
- return { instructions: [instruction] };
75
+ const withdrawInstruction = await buildWithdrawFromPositionInstruction(params);
76
+ const instructions: Instruction<string>[] = [withdrawInstruction];
77
+
78
+ const shouldUnwrapSol =
79
+ params.unwrapSol === true &&
80
+ params.vaultMint !== undefined &&
81
+ toAddress(params.vaultMint) === toAddress(NATIVE_MINT);
82
+
83
+ if (shouldUnwrapSol) {
84
+ instructions.push(
85
+ getCloseAccountInstruction(
86
+ params.lenderTokenAccount,
87
+ params.lender,
88
+ params.lender
89
+ )
90
+ );
91
+ }
92
+
93
+ return { instructions };
74
94
  }
package/omlp/service.ts CHANGED
@@ -12,10 +12,25 @@ import {
12
12
  type BuildWithdrawFromPositionParams,
13
13
  } from "./builders";
14
14
 
15
+ const INTEREST_FP_SCALE = 1_000_000_000_000n;
16
+
15
17
  function positiveDiff(a: bigint, b: bigint): bigint {
16
18
  return a > b ? a - b : 0n;
17
19
  }
18
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
+
19
34
  export async function depositToPosition(
20
35
  params: BuildDepositToPositionParams
21
36
  ) {
@@ -50,14 +65,23 @@ export async function withdrawAllFromPosition(
50
65
  position.totalInterestEarned,
51
66
  position.interestClaimed
52
67
  );
53
- const userMax = position.deposited + unclaimedInterest;
68
+ const pendingInterest = calculatePendingInterest(
69
+ position.deposited,
70
+ vault.accInterestPerShareFp,
71
+ position.interestIndexSnapshotFp
72
+ );
73
+ const userMax = position.deposited + unclaimedInterest + pendingInterest;
54
74
  const poolAvailable = positiveDiff(vault.totalLiquidity, vault.totalLoans);
55
75
  const amount = userMax < poolAvailable ? userMax : poolAvailable;
56
76
  if (amount <= 0n) {
57
77
  throw new Error("No withdrawable balance available right now.");
58
78
  }
59
79
 
60
- const built = await buildWithdrawFromPositionTransaction({ ...params, amount });
80
+ const built = await buildWithdrawFromPositionTransaction({
81
+ ...params,
82
+ amount,
83
+ vaultMint: vault.mint,
84
+ });
61
85
  return { instructions: built.instructions, amount };
62
86
  }
63
87
 
@@ -83,13 +107,26 @@ export async function withdrawInterestFromPosition(
83
107
  position.totalInterestEarned,
84
108
  position.interestClaimed
85
109
  );
110
+ const pendingInterest = calculatePendingInterest(
111
+ position.deposited,
112
+ vault.accInterestPerShareFp,
113
+ position.interestIndexSnapshotFp
114
+ );
115
+ const totalClaimableInterest = unclaimedInterest + pendingInterest;
86
116
  const poolAvailable = positiveDiff(vault.totalLiquidity, vault.totalLoans);
87
- const amount = unclaimedInterest < poolAvailable ? unclaimedInterest : poolAvailable;
117
+ const amount =
118
+ totalClaimableInterest < poolAvailable
119
+ ? totalClaimableInterest
120
+ : poolAvailable;
88
121
  if (amount <= 0n) {
89
122
  throw new Error("No claimable interest available right now.");
90
123
  }
91
124
 
92
- const built = await buildWithdrawFromPositionTransaction({ ...params, amount });
125
+ const built = await buildWithdrawFromPositionTransaction({
126
+ ...params,
127
+ amount,
128
+ vaultMint: vault.mint,
129
+ });
93
130
  return { instructions: built.instructions, amount };
94
131
  }
95
132
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@epicentral/sos-sdk",
3
- "version": "0.5.0-alpha.1",
3
+ "version": "0.5.0-alpha.10",
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
@@ -25,6 +25,7 @@ import {
25
25
  type RemainingAccountInput,
26
26
  } from "../shared/remaining-accounts";
27
27
  import {
28
+ getCloseAccountInstruction,
28
29
  NATIVE_MINT,
29
30
  getCreateAssociatedTokenIdempotentInstructionWithAddress,
30
31
  getWrapSOLInstructions,
@@ -75,6 +76,12 @@ export interface BuildOptionMintParams {
75
76
  escrowAuthority?: AddressLike;
76
77
  escrowTokenAccount?: AddressLike;
77
78
  poolLoan?: AddressLike;
79
+ /**
80
+ * When true (default), appends an SPL CloseAccount instruction after option_mint to close the
81
+ * maker's LONG token account (reclaim rent). The program transfers all LONG to escrow, so the
82
+ * maker's LONG ATA is left with zero balance and can be closed in the same transaction.
83
+ */
84
+ closeMakerLongAccount?: boolean;
78
85
  remainingAccounts?: RemainingAccountInput[];
79
86
  }
80
87
 
@@ -213,7 +220,21 @@ export async function buildOptionMintTransaction(
213
220
  params: BuildOptionMintParams
214
221
  ): Promise<BuiltTransaction> {
215
222
  const instruction = await buildOptionMintInstruction(params);
216
- return { instructions: [instruction] };
223
+ const instructions: Instruction<string>[] = [instruction];
224
+
225
+ const shouldCloseMakerLong =
226
+ params.closeMakerLongAccount !== false && params.makerLongAccount != null;
227
+ if (shouldCloseMakerLong) {
228
+ instructions.push(
229
+ getCloseAccountInstruction(
230
+ params.makerLongAccount!,
231
+ params.maker,
232
+ params.maker
233
+ )
234
+ );
235
+ }
236
+
237
+ return { instructions };
217
238
  }
218
239
 
219
240
  export interface BuildOptionMintTransactionWithDerivationParams {
@@ -468,6 +489,7 @@ export async function buildUnwindWriterUnsoldWithLoanRepayment(
468
489
  const writerRepaymentAccount =
469
490
  params.writerRepaymentAccount ??
470
491
  (await deriveAssociatedTokenAddress(params.writer, underlyingMint));
492
+ const writerDefaultRepaymentAta = await deriveAssociatedTokenAddress(params.writer, underlyingMint);
471
493
 
472
494
  const preflight = await preflightUnwindWriterUnsold({
473
495
  underlyingAsset: params.underlyingAsset,
@@ -480,10 +502,22 @@ export async function buildUnwindWriterUnsoldWithLoanRepayment(
480
502
  programId: params.programId,
481
503
  underlyingMint,
482
504
  });
505
+ const isWsolPath = toAddress(underlyingMint) === toAddress(NATIVE_MINT);
506
+ const lamportsToWrap =
507
+ preflight.summary.walletFallbackRequired > preflight.summary.walletFallbackAvailable
508
+ ? preflight.summary.walletFallbackRequired - preflight.summary.walletFallbackAvailable
509
+ : 0n;
510
+
483
511
  invariant(
484
512
  preflight.canRepayFully,
485
- `Unwind cannot fully repay loans: shortfall=${preflight.summary.shortfall}`
513
+ `Unwind cannot fully repay loans: required=${preflight.summary.proportionalTotalOwed} available_now=${preflight.summary.collateralVaultAvailable + preflight.summary.walletFallbackAvailable} native_sol_available=${preflight.summary.nativeSolAvailable} remaining_shortfall=${preflight.summary.proportionalTotalOwed - (preflight.summary.collateralVaultAvailable + preflight.summary.walletFallbackAvailable + preflight.summary.nativeSolAvailable)}`
486
514
  );
515
+ if (isWsolPath && lamportsToWrap > 0n && !params.includeWrapForShortfall) {
516
+ invariant(
517
+ false,
518
+ `Unwind requires WSOL top-up of ${lamportsToWrap} lamports. Rebuild with includeWrapForShortfall=true and writerSigner.`
519
+ );
520
+ }
487
521
 
488
522
  const unwindTx = await buildUnwindWriterUnsoldTransactionWithDerivation({
489
523
  underlyingAsset: params.underlyingAsset,
@@ -501,11 +535,11 @@ export async function buildUnwindWriterUnsoldWithLoanRepayment(
501
535
  remainingAccounts,
502
536
  });
503
537
 
504
- if (
505
- params.includeWrapForShortfall &&
506
- toAddress(underlyingMint) === toAddress(NATIVE_MINT) &&
507
- preflight.summary.walletFallbackRequired > 0n
508
- ) {
538
+ if (params.includeWrapForShortfall && isWsolPath && lamportsToWrap > 0n) {
539
+ invariant(
540
+ toAddress(writerRepaymentAccount) === toAddress(writerDefaultRepaymentAta),
541
+ "WSOL auto-wrap requires writerRepaymentAccount to be the writer WSOL ATA."
542
+ );
509
543
  invariant(
510
544
  !!params.writerSigner,
511
545
  "writerSigner is required when includeWrapForShortfall=true for WSOL shortfall top-up."
@@ -513,7 +547,7 @@ export async function buildUnwindWriterUnsoldWithLoanRepayment(
513
547
  const wrapInstructions = await getWrapSOLInstructions({
514
548
  payer: params.writerSigner,
515
549
  owner: params.writer,
516
- lamports: preflight.summary.walletFallbackRequired,
550
+ lamports: lamportsToWrap,
517
551
  });
518
552
  return {
519
553
  instructions: [...wrapInstructions, ...unwindTx.instructions],
@@ -6,6 +6,7 @@ import { fetchPoolLoansByMaker } from "../accounts/list";
6
6
  import { deriveAssociatedTokenAddress, deriveVaultPda, deriveWriterPositionPda } from "../accounts/pdas";
7
7
  import { resolveOptionAccounts } from "../accounts/resolve-option";
8
8
  import { invariant } from "../shared/errors";
9
+ import { NATIVE_MINT } from "../wsol/instructions";
9
10
 
10
11
  const TOKEN_ACCOUNT_AMOUNT_OFFSET = 64;
11
12
  const BPS_DENOMINATOR = 10_000n;
@@ -78,6 +79,10 @@ export interface UnwindPreflightSummary {
78
79
  /** For top-up UX: explicit shortfall fields */
79
80
  collateralVaultShortfall: bigint;
80
81
  needsWalletTopUp: boolean;
82
+ /** WSOL-only top-up metadata */
83
+ solTopUpRequired: bigint;
84
+ topUpRequiredForRepay: boolean;
85
+ nativeSolAvailable: bigint;
81
86
  }
82
87
 
83
88
  export interface UnwindPreflightResult {
@@ -111,9 +116,11 @@ export async function preflightUnwindWriterUnsold(
111
116
  const underlyingMint = params.underlyingMint ?? resolved.underlyingMint;
112
117
  const [vaultPda] = await deriveVaultPda(underlyingMint, params.programId);
113
118
  const vaultPdaAddress = toAddress(vaultPda);
119
+ const writerAddress = toAddress(params.writer);
120
+ const writerDefaultRepaymentAta = await deriveAssociatedTokenAddress(params.writer, underlyingMint);
114
121
  const writerRepaymentAccount =
115
122
  params.writerRepaymentAccount ??
116
- (await deriveAssociatedTokenAddress(params.writer, underlyingMint));
123
+ writerDefaultRepaymentAta;
117
124
  const writerRepaymentAddress = toAddress(writerRepaymentAccount);
118
125
  const [writerPositionAddress] = await deriveWriterPositionPda(
119
126
  resolved.optionPool,
@@ -162,6 +169,9 @@ export async function preflightUnwindWriterUnsold(
162
169
  shortfall: 0n,
163
170
  collateralVaultShortfall: 0n,
164
171
  needsWalletTopUp: false,
172
+ solTopUpRequired: 0n,
173
+ topUpRequiredForRepay: false,
174
+ nativeSolAvailable: 0n,
165
175
  },
166
176
  };
167
177
  }
@@ -192,6 +202,9 @@ export async function preflightUnwindWriterUnsold(
192
202
  shortfall: 0n,
193
203
  collateralVaultShortfall: 0n,
194
204
  needsWalletTopUp: false,
205
+ solTopUpRequired: 0n,
206
+ topUpRequiredForRepay: false,
207
+ nativeSolAvailable: 0n,
195
208
  },
196
209
  };
197
210
  }
@@ -242,10 +255,15 @@ export async function preflightUnwindWriterUnsold(
242
255
  { principal: 0n, interest: 0n, fees: 0n, owed: 0n }
243
256
  );
244
257
 
245
- const [collateralVaultAvailable, walletFallbackAvailable] = await Promise.all([
258
+ const isWsolRepaymentPath = toAddress(underlyingMint) === toAddress(NATIVE_MINT);
259
+ const canTopUpByWrapping =
260
+ isWsolRepaymentPath && writerRepaymentAddress === toAddress(writerDefaultRepaymentAta);
261
+ const [collateralVaultAvailable, walletFallbackAvailable, nativeBalanceResponse] = await Promise.all([
246
262
  fetchTokenAmount(params.rpc, resolved.collateralVault!),
247
263
  fetchTokenAmount(params.rpc, writerRepaymentAddress),
264
+ canTopUpByWrapping ? params.rpc.getBalance(writerAddress).send() : Promise.resolve({ value: 0n }),
248
265
  ]);
266
+ const nativeSolAvailable = nativeBalanceResponse.value;
249
267
 
250
268
  // Calculate proportional obligations for partial unwinds
251
269
  const writtenQty = toBigInt(writerPosition.writtenQty);
@@ -270,6 +288,12 @@ export async function preflightUnwindWriterUnsold(
270
288
  proportionalTotalOwed > collateralVaultAvailable ? proportionalTotalOwed - collateralVaultAvailable : 0n;
271
289
  const totalAvailable = collateralVaultAvailable + walletFallbackAvailable;
272
290
  const shortfall = proportionalTotalOwed > totalAvailable ? proportionalTotalOwed - totalAvailable : 0n;
291
+ const solTopUpRequired =
292
+ walletFallbackRequired > walletFallbackAvailable ? walletFallbackRequired - walletFallbackAvailable : 0n;
293
+ const topUpRequiredForRepay = solTopUpRequired > 0n;
294
+ const effectiveTotalAvailable = totalAvailable + (canTopUpByWrapping ? nativeSolAvailable : 0n);
295
+ const effectiveShortfall =
296
+ proportionalTotalOwed > effectiveTotalAvailable ? proportionalTotalOwed - effectiveTotalAvailable : 0n;
273
297
 
274
298
  // For top-up UX: explicit collateral vault shortfall
275
299
  const collateralVaultShortfall = returnableCollateral > collateralVaultAvailable
@@ -279,8 +303,11 @@ export async function preflightUnwindWriterUnsold(
279
303
 
280
304
  return {
281
305
  canUnwind: true,
282
- canRepayFully: shortfall === 0n,
283
- reason: shortfall === 0n ? undefined : "Insufficient combined collateral vault + writer fallback funds",
306
+ canRepayFully: effectiveShortfall === 0n,
307
+ reason:
308
+ effectiveShortfall === 0n
309
+ ? undefined
310
+ : "Insufficient combined collateral vault + wallet fallback funds (including SOL top-up capacity for WSOL)",
284
311
  writerPositionAddress: String(writerPositionAddress),
285
312
  writerRepaymentAccount: String(writerRepaymentAddress),
286
313
  collateralVaultAddress: String(resolved.collateralVault),
@@ -303,6 +330,9 @@ export async function preflightUnwindWriterUnsold(
303
330
  shortfall,
304
331
  collateralVaultShortfall,
305
332
  needsWalletTopUp,
333
+ solTopUpRequired,
334
+ topUpRequiredForRepay,
335
+ nativeSolAvailable,
306
336
  },
307
337
  };
308
338
  }