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

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
@@ -88,7 +88,11 @@ Borrow/repay for writers: use `buildOptionMintTransactionWithDerivation` (with v
88
88
 
89
89
  ## Unwind with Loan Repayment
90
90
 
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.
91
+ 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`:
92
+
93
+ 1. Collateral vault funds first.
94
+ 2. Writer fallback wallet source (`writerRepaymentAccount`) for any shortfall.
95
+ 3. If combined funds cannot cover principal + interest + protocol fees, unwind fails with a protocol custom error (not a generic SPL `0x1`).
92
96
 
93
97
  Use **`buildUnwindWriterUnsoldWithLoanRepayment`** so that:
94
98
 
@@ -97,12 +101,51 @@ Use **`buildUnwindWriterUnsoldWithLoanRepayment`** so that:
97
101
  3. `remaining_accounts` = **[PoolLoan₁, PoolLoan₂, ...]** only (capped at 20 loans per tx).
98
102
  4. One transaction burns, repays lenders from collateral vault, and returns collateral to the writer.
99
103
 
104
+ Use **`preflightUnwindWriterUnsold`** before building the transaction to get:
105
+
106
+ - Per-loan principal/interest/protocol-fee breakdown.
107
+ - Aggregate owed, collateral-vault available, wallet fallback required, and shortfall.
108
+ - `canRepayFully` so UI can block early with actionable messaging.
109
+
100
110
  If there are no active pool loans for that vault, the API still works and passes empty `remaining_accounts`.
101
111
 
102
112
  **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
113
 
104
114
  **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
115
 
116
+ ### Recommended Preflight + Unwind
117
+
118
+ ```ts
119
+ import {
120
+ preflightUnwindWriterUnsold,
121
+ buildUnwindWriterUnsoldWithLoanRepayment,
122
+ } from "@epicentral/sos-sdk";
123
+
124
+ const preflight = await preflightUnwindWriterUnsold({
125
+ underlyingAsset,
126
+ optionType,
127
+ strikePrice,
128
+ expirationDate,
129
+ writer,
130
+ unwindQty,
131
+ rpc,
132
+ });
133
+
134
+ if (!preflight.canRepayFully) {
135
+ throw new Error(`Unwind blocked. Shortfall: ${preflight.summary.shortfall.toString()}`);
136
+ }
137
+
138
+ const tx = await buildUnwindWriterUnsoldWithLoanRepayment({
139
+ underlyingAsset,
140
+ optionType,
141
+ strikePrice,
142
+ expirationDate,
143
+ writer,
144
+ unwindQty,
145
+ rpc,
146
+ });
147
+ ```
148
+
106
149
  ## Usage Examples
107
150
 
108
151
  ### Buy from pool (with derivation)
@@ -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
@@ -25,6 +25,7 @@ export * from "./short/builders";
25
25
  export * from "./short/claim-theta";
26
26
  export * from "./short/close-option";
27
27
  export * from "./short/pool";
28
+ export * from "./short/preflight";
28
29
 
29
30
  export * from "./omlp/builders";
30
31
  export * from "./omlp/service";
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.1",
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
+ }