@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 +85 -6
- package/client/lookup-table.ts +1 -1
- package/generated/errors/optionProgram.ts +12 -0
- package/generated/instructions/unwindWriterUnsold.ts +35 -4
- package/generated/programs/optionProgram.ts +2 -2
- package/index.ts +2 -0
- package/long/builders.ts +116 -2
- package/long/preflight.ts +114 -0
- package/package.json +1 -1
- package/short/builders.ts +83 -20
- package/short/preflight.ts +248 -0
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
|
|
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
|
|
153
|
+
### Buy From Pool (market order, high-level)
|
|
109
154
|
|
|
110
155
|
```ts
|
|
111
156
|
import {
|
|
112
|
-
|
|
113
|
-
|
|
157
|
+
buildBuyFromPoolMarketOrderTransactionWithDerivation,
|
|
158
|
+
preflightBuyFromPoolMarketOrder,
|
|
114
159
|
OptionType,
|
|
115
160
|
} from "@epicentral/sos-sdk";
|
|
116
161
|
|
|
117
|
-
const
|
|
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
|
-
|
|
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
|
package/client/lookup-table.ts
CHANGED
|
@@ -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("
|
|
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
|
-
|
|
639
|
-
|
|
640
|
-
|
|
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 <
|
|
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
|
-
"
|
|
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 = "
|
|
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
|
+
"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 {
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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
|
-
|
|
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
|
+
}
|