@epicentral/sos-sdk 0.6.2-alpha → 0.7.0-alpha

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
@@ -136,10 +136,25 @@ Or pass `addressLookupTableAddresses: [getLookupTableAddressForNetwork("devnet")
136
136
  | Function | Description |
137
137
  |----------|-------------|
138
138
  | `resolveSwitchboardFeedFromMarketData` | Resolves Switchboard feed address from market data account. |
139
- | `buildSwitchboardPullFeedUpdate` | Builds Switchboard pull-feed update instructions for prepending to trade transactions. |
139
+ | `buildSwitchboardPullFeedUpdate` | Low-level helper that fetches Switchboard pull-feed update instructions. |
140
+ | `buildSwitchboardCrank` | Returns Kit-native crank instructions plus lookup tables for the configured feed. |
141
+ | `prependSwitchboardCrank` | Prepends crank instructions/ALTs to a built SDK transaction. |
142
+
143
+ Price-sensitive option builders and OMLP deposit/withdraw builders now prepend Switchboard crank instructions by default, so frontends can keep using the same Kit transaction pipeline without managing a separate oracle update step.
140
144
 
141
145
  See [Frontend Switchboard Integration](../../docs/FRONTEND_SWITCHBOARD_INTEGRATION.md) for full setup and usage.
142
146
 
147
+ ### Global Trade Config
148
+
149
+ Use the shared trade config helpers to set SDK-wide defaults for slippage and compute-budget settings:
150
+
151
+ - `setGlobalTradeConfig`
152
+ - `updateGlobalTradeConfig`
153
+ - `getGlobalTradeConfig`
154
+ - `resetGlobalTradeConfig`
155
+
156
+ These defaults are used by the high-level builders unless an action-specific override is provided. `option_mint` also forwards a max-collateral bound on-chain so signer delays cannot silently widen required collateral past the configured tolerance.
157
+
143
158
  ## Multi-Collateral Settlement
144
159
 
145
160
  The SDK supports universal multi-collateral settlement, allowing writers to use ANY supported asset as collateral for options (not just the underlying). This enables:
@@ -215,7 +230,7 @@ When a writer unwinds an unsold short that had borrowed from the OMLP pool, the
215
230
  **Collateral Return:**
216
231
  - Proportional collateral share = `(collateral_deposited * unwind_qty) / written_qty`
217
232
  - Returnable collateral = `proportional_share - amount_already_repaid_from_vault`
218
- - If vault lacks sufficient post-repayment balance, fails with `InsufficientCollateralVault` (6090)
233
+ - If vault lacks sufficient post-repayment balance, fails with `InsufficientCollateralVault` (6092)
219
234
 
220
235
  Use **`buildUnwindWriterUnsoldWithLoanRepayment`** so that:
221
236
 
@@ -232,7 +247,8 @@ Use **`preflightUnwindWriterUnsold`** before building the transaction to get:
232
247
  - Collateral-vault available, wallet fallback required, and shortfall.
233
248
  - **Top-up UX fields:** `collateralVaultShortfall`, `needsWalletTopUp`.
234
249
  - WSOL repay metadata: `solTopUpRequired`, `topUpRequiredForRepay`, `nativeSolAvailable`.
235
- - `canRepayFully`, which now reflects effective repay solvency (including native SOL top-up capacity for WSOL paths).
250
+ - `canRepayRequestedSlice`, which reflects solvency for the proportional unwind slice (including native SOL top-up capacity for WSOL paths).
251
+ - Deprecated compatibility field: `canRepayFully` mirrors the slice-based result for older callers.
236
252
 
237
253
  If there are no active pool loans for that vault, the API still works and passes empty `remaining_accounts`.
238
254
 
@@ -333,7 +349,7 @@ The program uses distinct error codes for liquidity failures:
333
349
  - `InsufficientPoolAggregateLiquidity` (6042) – `option_pool.total_available < quantity`
334
350
  - `InsufficientWriterPositionLiquidity` (6043) – remaining writer-position accounts cannot cover full quantity in the smallest-first fill loop
335
351
 
336
- **Ghost Liquidity:** When `total_available` appears sufficient but active writer positions cannot cover the request. This happens when positions are settled/liquidated but still counted in the aggregate. The SDK now filters inactive positions, and the program skips them in the fill loop.
352
+ `option_pool.total_available` is now treated as writer-backed unsold inventory. `close_long_to_pool` no longer inflates the aggregate counter by re-adding buyer-closed LONGs, which removes the false-liquidity drift behind `InsufficientWriterPositionLiquidity`.
337
353
 
338
354
  **Recommended client flow:**
339
355
  1. Run `preflightBuyFromPoolMarketOrder` for UX gating (checks both pool and active writer liquidity).
@@ -365,7 +381,7 @@ The program uses the **Switchboard pull feed account** you pass in (or that the
365
381
  - **Buy:** Premium computation (Black-Scholes).
366
382
  - **Close:** Payout computation (mark-to-market). If the price is stale, the close payout will not reflect the current option value; the buyer may receive back only their premium instead of profit.
367
383
 
368
- **You must ensure the Switchboard feed is recently updated** when building buy and close transactions. The SDK does not post oracle updates by default; use the Switchboard helper exports and your update pipeline before trading instructions.
384
+ The SDK now prepends Switchboard crank instructions by default for mint, buy, close, unwind, exercise/settle/liquidate, and OMLP deposit/withdraw builders. Keep feeds configured correctly, but frontends no longer need a separate crank transaction in the standard path.
369
385
 
370
386
  - **Mainnet:** keep feed updates fresh enough to satisfy the feed's configured `max_staleness`.
371
387
  - **Devnet:** ensure your keeper/update pipeline runs before user trade flows; payouts reflect the feed's staleness config.
@@ -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("FTLPczoQpTmND126pMiJpnPudsDxfGuCNUQAPk4SUcJd"),
7
+ devnet: address("CxJn2cyXF8wRr4Cg7GKhSbGS7VW8wAnz3C4uQq537kXr"),
8
8
  mainnet: null,
9
9
  };
10
10
 
package/client/types.ts CHANGED
@@ -5,4 +5,5 @@ export type KitRpc = Rpc<SolanaRpcApi>;
5
5
 
6
6
  export interface BuiltTransaction {
7
7
  instructions: Instruction<string>[];
8
+ addressLookupTableAddresses?: AddressLike[];
8
9
  }
@@ -111,6 +111,11 @@ export type Vault = {
111
111
  * Set by admin, enforced on deposits. 0 = unlimited.
112
112
  */
113
113
  supplyLimit: bigint;
114
+ /**
115
+ * Maximum aggregate borrow cap as a share of total liquidity (basis points).
116
+ * Example: 9000 = at most 90% of liquidity may be loaned out.
117
+ */
118
+ maxBorrowCapBps: number;
114
119
  /**
115
120
  * Cumulative interest earned per deposited unit (fixed-point)
116
121
  * Increases whenever loans are repaid/liquidated
@@ -177,6 +182,11 @@ export type VaultArgs = {
177
182
  * Set by admin, enforced on deposits. 0 = unlimited.
178
183
  */
179
184
  supplyLimit: number | bigint;
185
+ /**
186
+ * Maximum aggregate borrow cap as a share of total liquidity (basis points).
187
+ * Example: 9000 = at most 90% of liquidity may be loaned out.
188
+ */
189
+ maxBorrowCapBps: number;
180
190
  /**
181
191
  * Cumulative interest earned per deposited unit (fixed-point)
182
192
  * Increases whenever loans are repaid/liquidated
@@ -208,6 +218,7 @@ export function getVaultEncoder(): FixedSizeEncoder<VaultArgs> {
208
218
  ["rateSlope1Bps", getU16Encoder()],
209
219
  ["rateSlope2Bps", getU16Encoder()],
210
220
  ["supplyLimit", getU64Encoder()],
221
+ ["maxBorrowCapBps", getU16Encoder()],
211
222
  ["accInterestPerShareFp", getU128Encoder()],
212
223
  ["lastInterestUpdateSlot", getU64Encoder()],
213
224
  ]),
@@ -236,6 +247,7 @@ export function getVaultDecoder(): FixedSizeDecoder<Vault> {
236
247
  ["rateSlope1Bps", getU16Decoder()],
237
248
  ["rateSlope2Bps", getU16Decoder()],
238
249
  ["supplyLimit", getU64Decoder()],
250
+ ["maxBorrowCapBps", getU16Decoder()],
239
251
  ["accInterestPerShareFp", getU128Decoder()],
240
252
  ["lastInterestUpdateSlot", getU64Decoder()],
241
253
  ]);
@@ -300,5 +312,5 @@ export async function fetchAllMaybeVault(
300
312
  }
301
313
 
302
314
  export function getVaultSize(): number {
303
- return 191;
315
+ return 193;
304
316
  }
@@ -23,6 +23,7 @@ export * from "./initializeMarketData";
23
23
  export * from "./initOptionPool";
24
24
  export * from "./liquidateWriterPosition";
25
25
  export * from "./omlpCreateVault";
26
+ export * from "./omlpUpdateMaxBorrowCap";
26
27
  export * from "./omlpUpdateMaxLeverage";
27
28
  export * from "./omlpUpdateProtocolFee";
28
29
  export * from "./omlpUpdateSupplyLimit";
@@ -107,6 +107,7 @@ export type OmlpCreateVaultInstruction<
107
107
  export type OmlpCreateVaultInstructionData = {
108
108
  discriminator: ReadonlyUint8Array;
109
109
  liqThresholdBps: number;
110
+ maxBorrowCapBps: number;
110
111
  maxLeverageMultiplier: number;
111
112
  baseRateBps: number;
112
113
  optimalUtilizationBps: number;
@@ -117,6 +118,7 @@ export type OmlpCreateVaultInstructionData = {
117
118
 
118
119
  export type OmlpCreateVaultInstructionDataArgs = {
119
120
  liqThresholdBps: number;
121
+ maxBorrowCapBps: number;
120
122
  maxLeverageMultiplier: number;
121
123
  baseRateBps: number;
122
124
  optimalUtilizationBps: number;
@@ -130,6 +132,7 @@ export function getOmlpCreateVaultInstructionDataEncoder(): FixedSizeEncoder<Oml
130
132
  getStructEncoder([
131
133
  ["discriminator", fixEncoderSize(getBytesEncoder(), 8)],
132
134
  ["liqThresholdBps", getU16Encoder()],
135
+ ["maxBorrowCapBps", getU16Encoder()],
133
136
  ["maxLeverageMultiplier", getU16Encoder()],
134
137
  ["baseRateBps", getU16Encoder()],
135
138
  ["optimalUtilizationBps", getU16Encoder()],
@@ -145,6 +148,7 @@ export function getOmlpCreateVaultInstructionDataDecoder(): FixedSizeDecoder<Oml
145
148
  return getStructDecoder([
146
149
  ["discriminator", fixDecoderSize(getBytesDecoder(), 8)],
147
150
  ["liqThresholdBps", getU16Decoder()],
151
+ ["maxBorrowCapBps", getU16Decoder()],
148
152
  ["maxLeverageMultiplier", getU16Decoder()],
149
153
  ["baseRateBps", getU16Decoder()],
150
154
  ["optimalUtilizationBps", getU16Decoder()],
@@ -189,6 +193,7 @@ export type OmlpCreateVaultAsyncInput<
189
193
  associatedTokenProgram?: Address<TAccountAssociatedTokenProgram>;
190
194
  systemProgram?: Address<TAccountSystemProgram>;
191
195
  liqThresholdBps: OmlpCreateVaultInstructionDataArgs["liqThresholdBps"];
196
+ maxBorrowCapBps: OmlpCreateVaultInstructionDataArgs["maxBorrowCapBps"];
192
197
  maxLeverageMultiplier: OmlpCreateVaultInstructionDataArgs["maxLeverageMultiplier"];
193
198
  baseRateBps: OmlpCreateVaultInstructionDataArgs["baseRateBps"];
194
199
  optimalUtilizationBps: OmlpCreateVaultInstructionDataArgs["optimalUtilizationBps"];
@@ -369,6 +374,7 @@ export type OmlpCreateVaultInput<
369
374
  associatedTokenProgram?: Address<TAccountAssociatedTokenProgram>;
370
375
  systemProgram?: Address<TAccountSystemProgram>;
371
376
  liqThresholdBps: OmlpCreateVaultInstructionDataArgs["liqThresholdBps"];
377
+ maxBorrowCapBps: OmlpCreateVaultInstructionDataArgs["maxBorrowCapBps"];
372
378
  maxLeverageMultiplier: OmlpCreateVaultInstructionDataArgs["maxLeverageMultiplier"];
373
379
  baseRateBps: OmlpCreateVaultInstructionDataArgs["baseRateBps"];
374
380
  optimalUtilizationBps: OmlpCreateVaultInstructionDataArgs["optimalUtilizationBps"];
@@ -0,0 +1,304 @@
1
+ /**
2
+ * This code was AUTOGENERATED using the Codama library.
3
+ * Please DO NOT EDIT THIS FILE, instead use visitors
4
+ * to add features, then rerun Codama to update it.
5
+ *
6
+ * @see https://github.com/codama-idl/codama
7
+ */
8
+
9
+ import {
10
+ combineCodec,
11
+ fixDecoderSize,
12
+ fixEncoderSize,
13
+ getBytesDecoder,
14
+ getBytesEncoder,
15
+ getProgramDerivedAddress,
16
+ getStructDecoder,
17
+ getStructEncoder,
18
+ getU16Decoder,
19
+ getU16Encoder,
20
+ transformEncoder,
21
+ type AccountMeta,
22
+ type AccountSignerMeta,
23
+ type Address,
24
+ type FixedSizeCodec,
25
+ type FixedSizeDecoder,
26
+ type FixedSizeEncoder,
27
+ type Instruction,
28
+ type InstructionWithAccounts,
29
+ type InstructionWithData,
30
+ type ReadonlyAccount,
31
+ type ReadonlySignerAccount,
32
+ type ReadonlyUint8Array,
33
+ type TransactionSigner,
34
+ type WritableAccount,
35
+ } from "@solana/kit";
36
+ import { OPTION_PROGRAM_PROGRAM_ADDRESS } from "../programs";
37
+ import { getAccountMetaFactory, type ResolvedAccount } from "../shared";
38
+
39
+ export const OMLP_UPDATE_MAX_BORROW_CAP_DISCRIMINATOR = new Uint8Array([
40
+ 156, 196, 135, 28, 127, 162, 101, 52,
41
+ ]);
42
+
43
+ export function getOmlpUpdateMaxBorrowCapDiscriminatorBytes() {
44
+ return fixEncoderSize(getBytesEncoder(), 8).encode(
45
+ OMLP_UPDATE_MAX_BORROW_CAP_DISCRIMINATOR,
46
+ );
47
+ }
48
+
49
+ export type OmlpUpdateMaxBorrowCapInstruction<
50
+ TProgram extends string = typeof OPTION_PROGRAM_PROGRAM_ADDRESS,
51
+ TAccountConfig extends string | AccountMeta<string> = string,
52
+ TAccountVault extends string | AccountMeta<string> = string,
53
+ TAccountAdmin extends string | AccountMeta<string> = string,
54
+ TRemainingAccounts extends readonly AccountMeta<string>[] = [],
55
+ > = Instruction<TProgram> &
56
+ InstructionWithData<ReadonlyUint8Array> &
57
+ InstructionWithAccounts<
58
+ [
59
+ TAccountConfig extends string
60
+ ? ReadonlyAccount<TAccountConfig>
61
+ : TAccountConfig,
62
+ TAccountVault extends string
63
+ ? WritableAccount<TAccountVault>
64
+ : TAccountVault,
65
+ TAccountAdmin extends string
66
+ ? ReadonlySignerAccount<TAccountAdmin> &
67
+ AccountSignerMeta<TAccountAdmin>
68
+ : TAccountAdmin,
69
+ ...TRemainingAccounts,
70
+ ]
71
+ >;
72
+
73
+ export type OmlpUpdateMaxBorrowCapInstructionData = {
74
+ discriminator: ReadonlyUint8Array;
75
+ newMaxBorrowCapBps: number;
76
+ };
77
+
78
+ export type OmlpUpdateMaxBorrowCapInstructionDataArgs = {
79
+ newMaxBorrowCapBps: number;
80
+ };
81
+
82
+ export function getOmlpUpdateMaxBorrowCapInstructionDataEncoder(): FixedSizeEncoder<OmlpUpdateMaxBorrowCapInstructionDataArgs> {
83
+ return transformEncoder(
84
+ getStructEncoder([
85
+ ["discriminator", fixEncoderSize(getBytesEncoder(), 8)],
86
+ ["newMaxBorrowCapBps", getU16Encoder()],
87
+ ]),
88
+ (value) => ({
89
+ ...value,
90
+ discriminator: OMLP_UPDATE_MAX_BORROW_CAP_DISCRIMINATOR,
91
+ }),
92
+ );
93
+ }
94
+
95
+ export function getOmlpUpdateMaxBorrowCapInstructionDataDecoder(): FixedSizeDecoder<OmlpUpdateMaxBorrowCapInstructionData> {
96
+ return getStructDecoder([
97
+ ["discriminator", fixDecoderSize(getBytesDecoder(), 8)],
98
+ ["newMaxBorrowCapBps", getU16Decoder()],
99
+ ]);
100
+ }
101
+
102
+ export function getOmlpUpdateMaxBorrowCapInstructionDataCodec(): FixedSizeCodec<
103
+ OmlpUpdateMaxBorrowCapInstructionDataArgs,
104
+ OmlpUpdateMaxBorrowCapInstructionData
105
+ > {
106
+ return combineCodec(
107
+ getOmlpUpdateMaxBorrowCapInstructionDataEncoder(),
108
+ getOmlpUpdateMaxBorrowCapInstructionDataDecoder(),
109
+ );
110
+ }
111
+
112
+ export type OmlpUpdateMaxBorrowCapAsyncInput<
113
+ TAccountConfig extends string = string,
114
+ TAccountVault extends string = string,
115
+ TAccountAdmin extends string = string,
116
+ > = {
117
+ /** Protocol config - stores admin authority */
118
+ config?: Address<TAccountConfig>;
119
+ vault: Address<TAccountVault>;
120
+ /** Admin signer - must match config.admin */
121
+ admin: TransactionSigner<TAccountAdmin>;
122
+ newMaxBorrowCapBps: OmlpUpdateMaxBorrowCapInstructionDataArgs["newMaxBorrowCapBps"];
123
+ };
124
+
125
+ export async function getOmlpUpdateMaxBorrowCapInstructionAsync<
126
+ TAccountConfig extends string,
127
+ TAccountVault extends string,
128
+ TAccountAdmin extends string,
129
+ TProgramAddress extends Address = typeof OPTION_PROGRAM_PROGRAM_ADDRESS,
130
+ >(
131
+ input: OmlpUpdateMaxBorrowCapAsyncInput<
132
+ TAccountConfig,
133
+ TAccountVault,
134
+ TAccountAdmin
135
+ >,
136
+ config?: { programAddress?: TProgramAddress },
137
+ ): Promise<
138
+ OmlpUpdateMaxBorrowCapInstruction<
139
+ TProgramAddress,
140
+ TAccountConfig,
141
+ TAccountVault,
142
+ TAccountAdmin
143
+ >
144
+ > {
145
+ // Program address.
146
+ const programAddress =
147
+ config?.programAddress ?? OPTION_PROGRAM_PROGRAM_ADDRESS;
148
+
149
+ // Original accounts.
150
+ const originalAccounts = {
151
+ config: { value: input.config ?? null, isWritable: false },
152
+ vault: { value: input.vault ?? null, isWritable: true },
153
+ admin: { value: input.admin ?? null, isWritable: false },
154
+ };
155
+ const accounts = originalAccounts as Record<
156
+ keyof typeof originalAccounts,
157
+ ResolvedAccount
158
+ >;
159
+
160
+ // Original args.
161
+ const args = { ...input };
162
+
163
+ // Resolve default values.
164
+ if (!accounts.config.value) {
165
+ accounts.config.value = await getProgramDerivedAddress({
166
+ programAddress,
167
+ seeds: [
168
+ getBytesEncoder().encode(new Uint8Array([99, 111, 110, 102, 105, 103])),
169
+ ],
170
+ });
171
+ }
172
+
173
+ const getAccountMeta = getAccountMetaFactory(programAddress, "programId");
174
+ return Object.freeze({
175
+ accounts: [
176
+ getAccountMeta(accounts.config),
177
+ getAccountMeta(accounts.vault),
178
+ getAccountMeta(accounts.admin),
179
+ ],
180
+ data: getOmlpUpdateMaxBorrowCapInstructionDataEncoder().encode(
181
+ args as OmlpUpdateMaxBorrowCapInstructionDataArgs,
182
+ ),
183
+ programAddress,
184
+ } as OmlpUpdateMaxBorrowCapInstruction<
185
+ TProgramAddress,
186
+ TAccountConfig,
187
+ TAccountVault,
188
+ TAccountAdmin
189
+ >);
190
+ }
191
+
192
+ export type OmlpUpdateMaxBorrowCapInput<
193
+ TAccountConfig extends string = string,
194
+ TAccountVault extends string = string,
195
+ TAccountAdmin extends string = string,
196
+ > = {
197
+ /** Protocol config - stores admin authority */
198
+ config: Address<TAccountConfig>;
199
+ vault: Address<TAccountVault>;
200
+ /** Admin signer - must match config.admin */
201
+ admin: TransactionSigner<TAccountAdmin>;
202
+ newMaxBorrowCapBps: OmlpUpdateMaxBorrowCapInstructionDataArgs["newMaxBorrowCapBps"];
203
+ };
204
+
205
+ export function getOmlpUpdateMaxBorrowCapInstruction<
206
+ TAccountConfig extends string,
207
+ TAccountVault extends string,
208
+ TAccountAdmin extends string,
209
+ TProgramAddress extends Address = typeof OPTION_PROGRAM_PROGRAM_ADDRESS,
210
+ >(
211
+ input: OmlpUpdateMaxBorrowCapInput<
212
+ TAccountConfig,
213
+ TAccountVault,
214
+ TAccountAdmin
215
+ >,
216
+ config?: { programAddress?: TProgramAddress },
217
+ ): OmlpUpdateMaxBorrowCapInstruction<
218
+ TProgramAddress,
219
+ TAccountConfig,
220
+ TAccountVault,
221
+ TAccountAdmin
222
+ > {
223
+ // Program address.
224
+ const programAddress =
225
+ config?.programAddress ?? OPTION_PROGRAM_PROGRAM_ADDRESS;
226
+
227
+ // Original accounts.
228
+ const originalAccounts = {
229
+ config: { value: input.config ?? null, isWritable: false },
230
+ vault: { value: input.vault ?? null, isWritable: true },
231
+ admin: { value: input.admin ?? null, isWritable: false },
232
+ };
233
+ const accounts = originalAccounts as Record<
234
+ keyof typeof originalAccounts,
235
+ ResolvedAccount
236
+ >;
237
+
238
+ // Original args.
239
+ const args = { ...input };
240
+
241
+ const getAccountMeta = getAccountMetaFactory(programAddress, "programId");
242
+ return Object.freeze({
243
+ accounts: [
244
+ getAccountMeta(accounts.config),
245
+ getAccountMeta(accounts.vault),
246
+ getAccountMeta(accounts.admin),
247
+ ],
248
+ data: getOmlpUpdateMaxBorrowCapInstructionDataEncoder().encode(
249
+ args as OmlpUpdateMaxBorrowCapInstructionDataArgs,
250
+ ),
251
+ programAddress,
252
+ } as OmlpUpdateMaxBorrowCapInstruction<
253
+ TProgramAddress,
254
+ TAccountConfig,
255
+ TAccountVault,
256
+ TAccountAdmin
257
+ >);
258
+ }
259
+
260
+ export type ParsedOmlpUpdateMaxBorrowCapInstruction<
261
+ TProgram extends string = typeof OPTION_PROGRAM_PROGRAM_ADDRESS,
262
+ TAccountMetas extends readonly AccountMeta[] = readonly AccountMeta[],
263
+ > = {
264
+ programAddress: Address<TProgram>;
265
+ accounts: {
266
+ /** Protocol config - stores admin authority */
267
+ config: TAccountMetas[0];
268
+ vault: TAccountMetas[1];
269
+ /** Admin signer - must match config.admin */
270
+ admin: TAccountMetas[2];
271
+ };
272
+ data: OmlpUpdateMaxBorrowCapInstructionData;
273
+ };
274
+
275
+ export function parseOmlpUpdateMaxBorrowCapInstruction<
276
+ TProgram extends string,
277
+ TAccountMetas extends readonly AccountMeta[],
278
+ >(
279
+ instruction: Instruction<TProgram> &
280
+ InstructionWithAccounts<TAccountMetas> &
281
+ InstructionWithData<ReadonlyUint8Array>,
282
+ ): ParsedOmlpUpdateMaxBorrowCapInstruction<TProgram, TAccountMetas> {
283
+ if (instruction.accounts.length < 3) {
284
+ // TODO: Coded error.
285
+ throw new Error("Not enough accounts");
286
+ }
287
+ let accountIndex = 0;
288
+ const getNextAccount = () => {
289
+ const accountMeta = (instruction.accounts as TAccountMetas)[accountIndex]!;
290
+ accountIndex += 1;
291
+ return accountMeta;
292
+ };
293
+ return {
294
+ programAddress: instruction.programAddress,
295
+ accounts: {
296
+ config: getNextAccount(),
297
+ vault: getNextAccount(),
298
+ admin: getNextAccount(),
299
+ },
300
+ data: getOmlpUpdateMaxBorrowCapInstructionDataDecoder().decode(
301
+ instruction.data,
302
+ ),
303
+ };
304
+ }
@@ -219,6 +219,7 @@ export type OptionMintInstructionData = {
219
219
  collateralMint: Address;
220
220
  makerCollateralAmount: bigint;
221
221
  borrowedAmount: bigint;
222
+ maxRequiredCollateralAmount: bigint;
222
223
  };
223
224
 
224
225
  export type OptionMintInstructionDataArgs = {
@@ -231,6 +232,7 @@ export type OptionMintInstructionDataArgs = {
231
232
  collateralMint: Address;
232
233
  makerCollateralAmount: number | bigint;
233
234
  borrowedAmount: number | bigint;
235
+ maxRequiredCollateralAmount: number | bigint;
234
236
  };
235
237
 
236
238
  export function getOptionMintInstructionDataEncoder(): Encoder<OptionMintInstructionDataArgs> {
@@ -249,6 +251,7 @@ export function getOptionMintInstructionDataEncoder(): Encoder<OptionMintInstruc
249
251
  ["collateralMint", getAddressEncoder()],
250
252
  ["makerCollateralAmount", getU64Encoder()],
251
253
  ["borrowedAmount", getU64Encoder()],
254
+ ["maxRequiredCollateralAmount", getU64Encoder()],
252
255
  ]),
253
256
  (value) => ({ ...value, discriminator: OPTION_MINT_DISCRIMINATOR }),
254
257
  );
@@ -269,6 +272,7 @@ export function getOptionMintInstructionDataDecoder(): Decoder<OptionMintInstruc
269
272
  ["collateralMint", getAddressDecoder()],
270
273
  ["makerCollateralAmount", getU64Decoder()],
271
274
  ["borrowedAmount", getU64Decoder()],
275
+ ["maxRequiredCollateralAmount", getU64Decoder()],
272
276
  ]);
273
277
  }
274
278
 
@@ -389,6 +393,7 @@ export type OptionMintAsyncInput<
389
393
  collateralMintArg: OptionMintInstructionDataArgs["collateralMint"];
390
394
  makerCollateralAmount: OptionMintInstructionDataArgs["makerCollateralAmount"];
391
395
  borrowedAmount: OptionMintInstructionDataArgs["borrowedAmount"];
396
+ maxRequiredCollateralAmount: OptionMintInstructionDataArgs["maxRequiredCollateralAmount"];
392
397
  };
393
398
 
394
399
  export async function getOptionMintInstructionAsync<
@@ -973,6 +978,7 @@ export type OptionMintInput<
973
978
  collateralMintArg: OptionMintInstructionDataArgs["collateralMint"];
974
979
  makerCollateralAmount: OptionMintInstructionDataArgs["makerCollateralAmount"];
975
980
  borrowedAmount: OptionMintInstructionDataArgs["borrowedAmount"];
981
+ maxRequiredCollateralAmount: OptionMintInstructionDataArgs["maxRequiredCollateralAmount"];
976
982
  };
977
983
 
978
984
  export function getOptionMintInstruction<
@@ -31,6 +31,7 @@ import {
31
31
  type ParsedInitOptionPoolInstruction,
32
32
  type ParsedLiquidateWriterPositionInstruction,
33
33
  type ParsedOmlpCreateVaultInstruction,
34
+ type ParsedOmlpUpdateMaxBorrowCapInstruction,
34
35
  type ParsedOmlpUpdateMaxLeverageInstruction,
35
36
  type ParsedOmlpUpdateProtocolFeeInstruction,
36
37
  type ParsedOmlpUpdateSupplyLimitInstruction,
@@ -51,7 +52,7 @@ import {
51
52
  } from "../instructions";
52
53
 
53
54
  export const OPTION_PROGRAM_PROGRAM_ADDRESS =
54
- "ASTGCefQVGEQ79ZSC6d2G9oMqmn2dk1onb4N75Bc5djQ" as Address<"ASTGCefQVGEQ79ZSC6d2G9oMqmn2dk1onb4N75Bc5djQ">;
55
+ "CCigZRDDjYiJCRL3FfkG3LbNYBpYaG6mGRiYGQnct1co" as Address<"CCigZRDDjYiJCRL3FfkG3LbNYBpYaG6mGRiYGQnct1co">;
55
56
 
56
57
  export enum OptionProgramAccount {
57
58
  CollateralPool,
@@ -227,6 +228,7 @@ export enum OptionProgramInstruction {
227
228
  InitializeMarketData,
228
229
  LiquidateWriterPosition,
229
230
  OmlpCreateVault,
231
+ OmlpUpdateMaxBorrowCap,
230
232
  OmlpUpdateMaxLeverage,
231
233
  OmlpUpdateProtocolFee,
232
234
  OmlpUpdateSupplyLimit,
@@ -437,6 +439,17 @@ export function identifyOptionProgramInstruction(
437
439
  ) {
438
440
  return OptionProgramInstruction.OmlpCreateVault;
439
441
  }
442
+ if (
443
+ containsBytes(
444
+ data,
445
+ fixEncoderSize(getBytesEncoder(), 8).encode(
446
+ new Uint8Array([156, 196, 135, 28, 127, 162, 101, 52]),
447
+ ),
448
+ 0,
449
+ )
450
+ ) {
451
+ return OptionProgramInstruction.OmlpUpdateMaxBorrowCap;
452
+ }
440
453
  if (
441
454
  containsBytes(
442
455
  data,
@@ -630,7 +643,7 @@ export function identifyOptionProgramInstruction(
630
643
  }
631
644
 
632
645
  export type ParsedOptionProgramInstruction<
633
- TProgram extends string = "ASTGCefQVGEQ79ZSC6d2G9oMqmn2dk1onb4N75Bc5djQ",
646
+ TProgram extends string = "CCigZRDDjYiJCRL3FfkG3LbNYBpYaG6mGRiYGQnct1co",
634
647
  > =
635
648
  | ({
636
649
  instructionType: OptionProgramInstruction.AcceptAdmin;
@@ -683,6 +696,9 @@ export type ParsedOptionProgramInstruction<
683
696
  | ({
684
697
  instructionType: OptionProgramInstruction.OmlpCreateVault;
685
698
  } & ParsedOmlpCreateVaultInstruction<TProgram>)
699
+ | ({
700
+ instructionType: OptionProgramInstruction.OmlpUpdateMaxBorrowCap;
701
+ } & ParsedOmlpUpdateMaxBorrowCapInstruction<TProgram>)
686
702
  | ({
687
703
  instructionType: OptionProgramInstruction.OmlpUpdateMaxLeverage;
688
704
  } & ParsedOmlpUpdateMaxLeverageInstruction<TProgram>)
package/index.ts CHANGED
@@ -30,11 +30,18 @@ export * from "./short/preflight";
30
30
 
31
31
  export * from "./omlp/builders";
32
32
  export * from "./omlp/service";
33
+ export * from "./shared/trade-config";
33
34
  export {
34
35
  resolveSwitchboardFeedFromMarketData,
35
36
  buildSwitchboardPullFeedUpdate,
37
+ buildSwitchboardCrank,
38
+ inferSwitchboardNetwork,
39
+ prependSwitchboardCrank,
36
40
  type SwitchboardPullFeedLike,
37
41
  type BuildSwitchboardPullFeedUpdateParams,
42
+ type BuildSwitchboardCrankParams,
43
+ type SwitchboardCrankResult,
44
+ type SwitchboardNetwork,
38
45
  } from "./oracle/switchboard";
39
46
 
40
47
  export {
package/long/builders.ts CHANGED
@@ -24,7 +24,13 @@ import {
24
24
  } from "../wsol/instructions";
25
25
  import { fetchMarketDataAccount, fetchOptionPool } from "../accounts/fetchers";
26
26
  import { getBuyFromPoolRemainingAccounts } from "./remaining-accounts";
27
+ import { applySlippageBps } from "./quotes";
28
+ import {
29
+ buildSwitchboardCrank,
30
+ prependSwitchboardCrank,
31
+ } from "../oracle/switchboard";
27
32
  import { fetchWriterPositionsForPool } from "../accounts/list";
33
+ import { getGlobalTradeConfig } from "../shared/trade-config";
28
34
  import bs58 from "bs58";
29
35
 
30
36
  export interface BuildBuyFromPoolParams {
@@ -143,6 +149,9 @@ export interface BuildBuyFromPoolTransactionWithDerivationParams {
143
149
  buyerPosition?: AddressLike;
144
150
  buyerOptionAccount?: AddressLike;
145
151
  remainingAccounts?: RemainingAccountInput[];
152
+ disableSwitchboardCrank?: boolean;
153
+ switchboardCrossbarUrl?: string;
154
+ switchboardNumSignatures?: number;
146
155
  }
147
156
 
148
157
  const DEFAULT_MARKET_ORDER_SLIPPAGE_BUFFER_BASE_UNITS = 500_000n;
@@ -224,7 +233,7 @@ export async function buildBuyFromPoolTransactionWithDerivation(
224
233
  Array.from(marketDataAccount.switchboardFeedId as unknown as Uint8Array)
225
234
  );
226
235
 
227
- return buildBuyFromPoolTransaction({
236
+ const actionTx = await buildBuyFromPoolTransaction({
228
237
  optionPool: resolved.optionPool,
229
238
  optionAccount: resolved.optionAccount,
230
239
  longMint: resolved.longMint,
@@ -241,6 +250,20 @@ export async function buildBuyFromPoolTransactionWithDerivation(
241
250
  buyerOptionAccount,
242
251
  remainingAccounts: params.remainingAccounts,
243
252
  });
253
+
254
+ if (params.disableSwitchboardCrank) {
255
+ return actionTx;
256
+ }
257
+
258
+ const crank = await buildSwitchboardCrank({
259
+ rpc: params.rpc,
260
+ payer: params.buyer,
261
+ switchboardFeed,
262
+ marketData: resolved.marketData,
263
+ crossbarUrl: params.switchboardCrossbarUrl,
264
+ numSignatures: params.switchboardNumSignatures,
265
+ });
266
+ return prependSwitchboardCrank(crank, actionTx);
244
267
  }
245
268
 
246
269
  export interface BuildBuyFromPoolMarketOrderParams
@@ -311,10 +334,16 @@ export async function buildBuyFromPoolMarketOrderTransactionWithDerivation(
311
334
  `This may indicate data staleness - please refresh and retry.`
312
335
  );
313
336
 
314
- const slippageBuffer = normalizeMarketOrderSlippageBuffer(
315
- params,
316
- refetchedPool.underlyingMint
317
- );
337
+ const globalTradeConfig = getGlobalTradeConfig();
338
+ const hasExplicitSlippageBuffer =
339
+ params.slippageBufferBaseUnits !== undefined ||
340
+ params.slippageBufferLamports !== undefined;
341
+ const slippageBuffer = hasExplicitSlippageBuffer
342
+ ? normalizeMarketOrderSlippageBuffer(params, refetchedPool.underlyingMint)
343
+ : globalTradeConfig.slippageBps !== undefined
344
+ ? applySlippageBps(params.quotedPremiumTotal, globalTradeConfig.slippageBps) -
345
+ BigInt(params.quotedPremiumTotal)
346
+ : normalizeMarketOrderSlippageBuffer(params, refetchedPool.underlyingMint);
318
347
  const maxPremiumAmount = BigInt(params.quotedPremiumTotal) + slippageBuffer;
319
348
  assertPositiveAmount(maxPremiumAmount, "maxPremiumAmount");
320
349
 
@@ -329,7 +358,7 @@ export async function buildBuyFromPoolMarketOrderTransactionWithDerivation(
329
358
  Array.from(marketDataAccount.switchboardFeedId as unknown as Uint8Array)
330
359
  );
331
360
 
332
- return buildBuyFromPoolTransaction({
361
+ const actionTx = await buildBuyFromPoolTransaction({
333
362
  optionPool: resolved.optionPool,
334
363
  optionAccount: resolved.optionAccount,
335
364
  longMint: resolved.longMint,
@@ -346,6 +375,20 @@ export async function buildBuyFromPoolMarketOrderTransactionWithDerivation(
346
375
  buyerOptionAccount,
347
376
  remainingAccounts,
348
377
  });
378
+
379
+ if (params.disableSwitchboardCrank) {
380
+ return actionTx;
381
+ }
382
+
383
+ const crank = await buildSwitchboardCrank({
384
+ rpc: params.rpc,
385
+ payer: params.buyer,
386
+ switchboardFeed,
387
+ marketData: resolved.marketData,
388
+ crossbarUrl: params.switchboardCrossbarUrl,
389
+ numSignatures: params.switchboardNumSignatures,
390
+ });
391
+ return prependSwitchboardCrank(crank, actionTx);
349
392
  }
350
393
 
351
394
  export async function buildCloseLongToPoolInstruction(
@@ -438,6 +481,9 @@ export interface BuildCloseLongToPoolTransactionWithDerivationParams {
438
481
  */
439
482
  unwrapPayoutSol?: boolean;
440
483
  remainingAccounts?: RemainingAccountInput[];
484
+ disableSwitchboardCrank?: boolean;
485
+ switchboardCrossbarUrl?: string;
486
+ switchboardNumSignatures?: number;
441
487
  }
442
488
 
443
489
  export async function buildCloseLongToPoolTransactionWithDerivation(
@@ -485,7 +531,7 @@ export async function buildCloseLongToPoolTransactionWithDerivation(
485
531
  Array.from(marketDataAccount.switchboardFeedId as unknown as Uint8Array)
486
532
  );
487
533
 
488
- return buildCloseLongToPoolTransaction({
534
+ const actionTx = await buildCloseLongToPoolTransaction({
489
535
  optionPool: resolved.optionPool,
490
536
  optionAccount: resolved.optionAccount,
491
537
  collateralPool: resolved.collateralPool,
@@ -507,4 +553,18 @@ export async function buildCloseLongToPoolTransactionWithDerivation(
507
553
  unwrapPayoutSol,
508
554
  remainingAccounts: params.remainingAccounts,
509
555
  });
556
+
557
+ if (params.disableSwitchboardCrank) {
558
+ return actionTx;
559
+ }
560
+
561
+ const crank = await buildSwitchboardCrank({
562
+ rpc: params.rpc,
563
+ payer: params.buyer,
564
+ switchboardFeed,
565
+ marketData: resolved.marketData,
566
+ crossbarUrl: params.switchboardCrossbarUrl,
567
+ numSignatures: params.switchboardNumSignatures,
568
+ });
569
+ return prependSwitchboardCrank(crank, actionTx);
510
570
  }
package/long/exercise.ts CHANGED
@@ -1,7 +1,11 @@
1
1
  import { getOptionExerciseInstruction } from "../generated/instructions";
2
2
  import type { Instruction } from "@solana/kit";
3
3
  import { toAddress } from "../client/program";
4
- import type { AddressLike, BuiltTransaction } from "../client/types";
4
+ import type { AddressLike, BuiltTransaction, KitRpc } from "../client/types";
5
+ import {
6
+ buildSwitchboardCrank,
7
+ prependSwitchboardCrank,
8
+ } from "../oracle/switchboard";
5
9
 
6
10
  export interface BuildOptionExerciseParams {
7
11
  optionAccount: AddressLike;
@@ -16,6 +20,10 @@ export interface BuildOptionExerciseParams {
16
20
  escrowAuthority: AddressLike;
17
21
  buyer: AddressLike;
18
22
  tokenProgram?: AddressLike;
23
+ rpc?: KitRpc;
24
+ disableSwitchboardCrank?: boolean;
25
+ switchboardCrossbarUrl?: string;
26
+ switchboardNumSignatures?: number;
19
27
  }
20
28
 
21
29
  /**
@@ -48,7 +56,18 @@ export function buildOptionExerciseInstruction(
48
56
  */
49
57
  export function buildOptionExerciseTransaction(
50
58
  params: BuildOptionExerciseParams
51
- ): BuiltTransaction {
59
+ ): Promise<BuiltTransaction> {
52
60
  const instruction = buildOptionExerciseInstruction(params);
53
- return { instructions: [instruction] };
61
+ const actionTx = { instructions: [instruction] };
62
+ if (params.disableSwitchboardCrank || !params.rpc) {
63
+ return Promise.resolve(actionTx);
64
+ }
65
+
66
+ return buildSwitchboardCrank({
67
+ rpc: params.rpc,
68
+ payer: params.buyer,
69
+ switchboardFeed: params.switchboardFeed,
70
+ crossbarUrl: params.switchboardCrossbarUrl,
71
+ numSignatures: params.switchboardNumSignatures,
72
+ }).then((crank) => prependSwitchboardCrank(crank, actionTx));
54
73
  }
package/omlp/builders.ts CHANGED
@@ -4,9 +4,13 @@ import {
4
4
  } from "../generated/instructions";
5
5
  import type { Instruction } from "@solana/kit";
6
6
  import { toAddress } from "../client/program";
7
- import type { AddressLike, BuiltTransaction } from "../client/types";
7
+ import type { AddressLike, BuiltTransaction, KitRpc } from "../client/types";
8
8
  import { assertPositiveAmount } from "../shared/amounts";
9
9
  import { getCloseAccountInstruction, NATIVE_MINT } from "../wsol/instructions";
10
+ import {
11
+ buildSwitchboardCrank,
12
+ prependSwitchboardCrank,
13
+ } from "../oracle/switchboard";
10
14
 
11
15
  export interface BuildDepositToPositionParams {
12
16
  vault: AddressLike;
@@ -15,6 +19,12 @@ export interface BuildDepositToPositionParams {
15
19
  lender: AddressLike;
16
20
  amount: bigint | number;
17
21
  position?: AddressLike;
22
+ rpc?: KitRpc;
23
+ switchboardFeed?: AddressLike;
24
+ marketData?: AddressLike;
25
+ disableSwitchboardCrank?: boolean;
26
+ switchboardCrossbarUrl?: string;
27
+ switchboardNumSignatures?: number;
18
28
  }
19
29
 
20
30
  export interface BuildWithdrawFromPositionParams {
@@ -26,6 +36,12 @@ export interface BuildWithdrawFromPositionParams {
26
36
  position?: AddressLike;
27
37
  unwrapSol?: boolean;
28
38
  vaultMint?: AddressLike;
39
+ rpc?: KitRpc;
40
+ switchboardFeed?: AddressLike;
41
+ marketData?: AddressLike;
42
+ disableSwitchboardCrank?: boolean;
43
+ switchboardCrossbarUrl?: string;
44
+ switchboardNumSignatures?: number;
29
45
  }
30
46
 
31
47
  export async function buildDepositToPositionInstruction(
@@ -49,7 +65,24 @@ export async function buildDepositToPositionTransaction(
49
65
  params: BuildDepositToPositionParams
50
66
  ): Promise<BuiltTransaction> {
51
67
  const instruction = await buildDepositToPositionInstruction(params);
52
- return { instructions: [instruction] };
68
+ const actionTx = { instructions: [instruction] };
69
+ if (
70
+ params.disableSwitchboardCrank ||
71
+ !params.rpc ||
72
+ (!params.switchboardFeed && !params.marketData)
73
+ ) {
74
+ return actionTx;
75
+ }
76
+
77
+ const crank = await buildSwitchboardCrank({
78
+ rpc: params.rpc,
79
+ payer: params.lender,
80
+ switchboardFeed: params.switchboardFeed,
81
+ marketData: params.marketData,
82
+ crossbarUrl: params.switchboardCrossbarUrl,
83
+ numSignatures: params.switchboardNumSignatures,
84
+ });
85
+ return prependSwitchboardCrank(crank, actionTx);
53
86
  }
54
87
 
55
88
  export async function buildWithdrawFromPositionInstruction(
@@ -90,5 +123,22 @@ export async function buildWithdrawFromPositionTransaction(
90
123
  );
91
124
  }
92
125
 
93
- return { instructions };
126
+ const actionTx = { instructions };
127
+ if (
128
+ params.disableSwitchboardCrank ||
129
+ !params.rpc ||
130
+ (!params.switchboardFeed && !params.marketData)
131
+ ) {
132
+ return actionTx;
133
+ }
134
+
135
+ const crank = await buildSwitchboardCrank({
136
+ rpc: params.rpc,
137
+ payer: params.lender,
138
+ switchboardFeed: params.switchboardFeed,
139
+ marketData: params.marketData,
140
+ crossbarUrl: params.switchboardCrossbarUrl,
141
+ numSignatures: params.switchboardNumSignatures,
142
+ });
143
+ return prependSwitchboardCrank(crank, actionTx);
94
144
  }
@@ -1,10 +1,17 @@
1
- import type { Address } from "@solana/kit";
1
+ import type { Address, Instruction } from "@solana/kit";
2
+ import { fromLegacyTransactionInstruction } from "@solana/compat";
3
+ import { CrossbarClient } from "@switchboard-xyz/common";
2
4
  import bs58 from "bs58";
3
5
  import { toAddress } from "../client/program";
4
- import type { AddressLike, KitRpc } from "../client/types";
6
+ import type { AddressLike, BuiltTransaction, KitRpc } from "../client/types";
5
7
  import { fetchMarketDataAccount } from "../accounts/fetchers";
6
8
  import { invariant } from "../shared/errors";
7
9
 
10
+ const DEVNET_GENESIS_HASH = "EtWTRABZaYq6iMfeYKouRu166VU2xqa1wcaWoxPkrZBG";
11
+ const MAINNET_BETA_GENESIS_HASH = "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpKuc147dw2N9d";
12
+
13
+ export type SwitchboardNetwork = "devnet" | "mainnet";
14
+
8
15
  export async function resolveSwitchboardFeedFromMarketData(
9
16
  rpc: KitRpc,
10
17
  marketData: AddressLike
@@ -54,3 +61,84 @@ export async function buildSwitchboardPullFeedUpdate<
54
61
  lookupTables: luts ?? [],
55
62
  };
56
63
  }
64
+
65
+ export interface BuildSwitchboardCrankParams {
66
+ rpc: KitRpc;
67
+ payer: AddressLike;
68
+ switchboardFeed?: AddressLike;
69
+ marketData?: AddressLike;
70
+ network?: SwitchboardNetwork;
71
+ crossbarUrl?: string;
72
+ numSignatures?: number;
73
+ }
74
+
75
+ export interface SwitchboardCrankResult {
76
+ instructions: Instruction<string>[];
77
+ addressLookupTableAddresses: AddressLike[];
78
+ }
79
+
80
+ export async function inferSwitchboardNetwork(
81
+ rpc: KitRpc
82
+ ): Promise<SwitchboardNetwork> {
83
+ const genesisHash = await rpc.getGenesisHash().send();
84
+ if (genesisHash === DEVNET_GENESIS_HASH) {
85
+ return "devnet";
86
+ }
87
+ if (genesisHash === MAINNET_BETA_GENESIS_HASH) {
88
+ return "mainnet";
89
+ }
90
+ throw new Error(
91
+ `Unable to infer Switchboard network from genesis hash: ${genesisHash}`
92
+ );
93
+ }
94
+
95
+ export async function buildSwitchboardCrank(
96
+ params: BuildSwitchboardCrankParams
97
+ ): Promise<SwitchboardCrankResult> {
98
+ const resolvedFeed =
99
+ params.switchboardFeed ??
100
+ (params.marketData
101
+ ? await resolveSwitchboardFeedFromMarketData(params.rpc, params.marketData)
102
+ : undefined);
103
+
104
+ invariant(
105
+ !!resolvedFeed,
106
+ "switchboardFeed or marketData is required to build Switchboard crank instructions."
107
+ );
108
+
109
+ const network = params.network ?? (await inferSwitchboardNetwork(params.rpc));
110
+ const crossbar = params.crossbarUrl
111
+ ? new CrossbarClient(params.crossbarUrl)
112
+ : CrossbarClient.default();
113
+ const updates = await crossbar.fetchSolanaUpdates(
114
+ network,
115
+ [toAddress(resolvedFeed)],
116
+ toAddress(params.payer),
117
+ params.numSignatures
118
+ );
119
+ const update = updates[0];
120
+
121
+ const instructions =
122
+ update?.pullIxns?.map((instruction) =>
123
+ fromLegacyTransactionInstruction(instruction)
124
+ ) ?? [];
125
+ const addressLookupTableAddresses = update?.lookupTables ?? [];
126
+
127
+ return {
128
+ instructions,
129
+ addressLookupTableAddresses,
130
+ };
131
+ }
132
+
133
+ export function prependSwitchboardCrank(
134
+ crank: SwitchboardCrankResult,
135
+ action: BuiltTransaction
136
+ ): BuiltTransaction {
137
+ return {
138
+ instructions: [...crank.instructions, ...action.instructions],
139
+ addressLookupTableAddresses: [
140
+ ...(crank.addressLookupTableAddresses ?? []),
141
+ ...(action.addressLookupTableAddresses ?? []),
142
+ ],
143
+ };
144
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@epicentral/sos-sdk",
3
- "version": "0.6.2-alpha",
3
+ "version": "0.7.0-alpha",
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",
@@ -18,7 +18,10 @@
18
18
  "@solana-program/address-lookup-table": "^0.11.0",
19
19
  "@solana-program/compute-budget": "^0.13.0",
20
20
  "@solana-program/system": "^0.11.0",
21
+ "@solana/compat": "^6.1.0",
21
22
  "@solana/kit": "^6.1.0",
23
+ "@switchboard-xyz/common": "^5.7.0",
24
+ "@switchboard-xyz/on-demand": "^3.9.0",
22
25
  "bs58": "^6.0.0",
23
26
  "decimal.js": "^10.4.3"
24
27
  },
@@ -0,0 +1,27 @@
1
+ export interface TradeConfig {
2
+ slippageBps?: number;
3
+ computeUnitLimit?: number;
4
+ computeUnitPriceMicroLamports?: number;
5
+ }
6
+
7
+ let globalTradeConfig: TradeConfig = {};
8
+
9
+ export function setGlobalTradeConfig(config: TradeConfig): void {
10
+ globalTradeConfig = { ...config };
11
+ }
12
+
13
+ export function updateGlobalTradeConfig(config: Partial<TradeConfig>): void {
14
+ globalTradeConfig = { ...globalTradeConfig, ...config };
15
+ }
16
+
17
+ export function getGlobalTradeConfig(): TradeConfig {
18
+ return { ...globalTradeConfig };
19
+ }
20
+
21
+ export function resetGlobalTradeConfig(): void {
22
+ globalTradeConfig = {};
23
+ }
24
+
25
+ export function resolveTradeConfig(overrides?: Partial<TradeConfig>): TradeConfig {
26
+ return { ...globalTradeConfig, ...overrides };
27
+ }
@@ -25,6 +25,7 @@ import {
25
25
  getLookupTableAddressForNetwork,
26
26
  type LookupTableNetwork,
27
27
  } from "../client/lookup-table";
28
+ import { getGlobalTradeConfig } from "./trade-config";
28
29
 
29
30
  export interface SendBuiltTransactionParams extends BuiltTransaction {
30
31
  rpc: KitRpc;
@@ -56,18 +57,24 @@ export async function sendBuiltTransaction(
56
57
  params: SendBuiltTransactionParams
57
58
  ): Promise<string> {
58
59
  const commitment = params.commitment ?? "confirmed";
60
+ const globalTradeConfig = getGlobalTradeConfig();
59
61
  const { value: latestBlockhash } = await params.rpc.getLatestBlockhash().send();
60
62
 
61
63
  const computeBudgetInstructions: Instruction<string>[] = [];
62
- if (params.computeUnitLimit !== undefined) {
64
+ const computeUnitLimit =
65
+ params.computeUnitLimit ?? globalTradeConfig.computeUnitLimit;
66
+ if (computeUnitLimit !== undefined) {
63
67
  computeBudgetInstructions.push(
64
- getSetComputeUnitLimitInstruction({ units: params.computeUnitLimit })
68
+ getSetComputeUnitLimitInstruction({ units: computeUnitLimit })
65
69
  );
66
70
  }
67
- if (params.computeUnitPriceMicroLamports !== undefined) {
71
+ const computeUnitPriceMicroLamports =
72
+ params.computeUnitPriceMicroLamports ??
73
+ globalTradeConfig.computeUnitPriceMicroLamports;
74
+ if (computeUnitPriceMicroLamports !== undefined) {
68
75
  computeBudgetInstructions.push(
69
76
  getSetComputeUnitPriceInstruction({
70
- microLamports: params.computeUnitPriceMicroLamports,
77
+ microLamports: computeUnitPriceMicroLamports,
71
78
  })
72
79
  );
73
80
  }
package/short/builders.ts CHANGED
@@ -31,6 +31,12 @@ import {
31
31
  getWrapSOLInstructions,
32
32
  } from "../wsol/instructions";
33
33
  import { preflightUnwindWriterUnsold } from "./preflight";
34
+ import {
35
+ buildSwitchboardCrank,
36
+ prependSwitchboardCrank,
37
+ } from "../oracle/switchboard";
38
+ import { applySlippageBps } from "../long/quotes";
39
+ import { getGlobalTradeConfig } from "../shared/trade-config";
34
40
  import bs58 from "bs58";
35
41
 
36
42
  export interface BuildOptionMintParams {
@@ -48,6 +54,7 @@ export interface BuildOptionMintParams {
48
54
  collateralMint?: AddressLike;
49
55
  makerCollateralAmount: bigint | number;
50
56
  borrowedAmount: bigint | number;
57
+ maxRequiredCollateralAmount?: bigint | number;
51
58
  maker: AddressLike;
52
59
  makerCollateralAccount: AddressLike;
53
60
  underlyingMint: AddressLike;
@@ -133,6 +140,16 @@ export async function buildOptionMintInstruction(
133
140
  invariant(params.underlyingSymbol.length > 0, "underlyingSymbol is required.");
134
141
 
135
142
  const borrowedAmount = BigInt(params.borrowedAmount);
143
+ const globalTradeConfig = getGlobalTradeConfig();
144
+ const maxRequiredCollateralAmount =
145
+ params.maxRequiredCollateralAmount !== undefined
146
+ ? BigInt(params.maxRequiredCollateralAmount)
147
+ : globalTradeConfig.slippageBps !== undefined
148
+ ? applySlippageBps(
149
+ BigInt(params.makerCollateralAmount) + borrowedAmount,
150
+ globalTradeConfig.slippageBps
151
+ )
152
+ : BigInt(params.makerCollateralAmount) + borrowedAmount;
136
153
  if (borrowedAmount > 0n) {
137
154
  invariant(!!params.vault, "vault is required when borrowedAmount > 0");
138
155
  invariant(
@@ -209,6 +226,7 @@ export async function buildOptionMintInstruction(
209
226
  collateralMintArg: toAddress(params.collateralMint ?? params.underlyingMint),
210
227
  makerCollateralAmount: params.makerCollateralAmount,
211
228
  borrowedAmount: params.borrowedAmount,
229
+ maxRequiredCollateralAmount,
212
230
  });
213
231
 
214
232
  return appendRemainingAccounts(kitInstruction, params.remainingAccounts);
@@ -251,6 +269,7 @@ export interface BuildOptionMintTransactionWithDerivationParams {
251
269
  collateralMint?: AddressLike;
252
270
  makerCollateralAmount: bigint | number;
253
271
  borrowedAmount: bigint | number;
272
+ maxRequiredCollateralAmount?: bigint | number;
254
273
  maker: AddressLike;
255
274
  /**
256
275
  * Optional. When omitted, the SDK derives the maker's collateral ATA for collateralMint
@@ -268,6 +287,9 @@ export interface BuildOptionMintTransactionWithDerivationParams {
268
287
  escrowTokenAccount?: AddressLike;
269
288
  poolLoan?: AddressLike;
270
289
  remainingAccounts?: RemainingAccountInput[];
290
+ disableSwitchboardCrank?: boolean;
291
+ switchboardCrossbarUrl?: string;
292
+ switchboardNumSignatures?: number;
271
293
  }
272
294
 
273
295
  export async function buildOptionMintTransactionWithDerivation(
@@ -357,9 +379,23 @@ export async function buildOptionMintTransactionWithDerivation(
357
379
  makerCollateralAccount
358
380
  );
359
381
 
360
- return {
382
+ const actionTx = {
361
383
  instructions: [createAtaIx, ...tx.instructions],
362
384
  };
385
+
386
+ if (params.disableSwitchboardCrank) {
387
+ return actionTx;
388
+ }
389
+
390
+ const crank = await buildSwitchboardCrank({
391
+ rpc: params.rpc,
392
+ payer: params.maker,
393
+ switchboardFeed,
394
+ marketData: resolved.marketData,
395
+ crossbarUrl: params.switchboardCrossbarUrl,
396
+ numSignatures: params.switchboardNumSignatures,
397
+ });
398
+ return prependSwitchboardCrank(crank, actionTx);
363
399
  }
364
400
 
365
401
  export async function buildUnwindWriterUnsoldInstruction(
@@ -480,8 +516,12 @@ export async function buildUnwindWriterUnsoldWithLoanRepayment(
480
516
  ]);
481
517
 
482
518
  const vaultLoans = loans
483
- .filter((item) => toAddress(item.data.vault) === vaultPdaStr)
484
- .slice(0, MAX_POOL_LOANS_PER_UNWIND);
519
+ .filter((item) => toAddress(item.data.vault) === vaultPdaStr);
520
+
521
+ invariant(
522
+ vaultLoans.length <= MAX_POOL_LOANS_PER_UNWIND,
523
+ `Too many active pool loans for unwind: ${vaultLoans.length}. Max supported in one unwind is ${MAX_POOL_LOANS_PER_UNWIND}.`
524
+ );
485
525
 
486
526
  const remainingAccounts: RemainingAccountInput[] = vaultLoans.map((item) => ({
487
527
  address: item.address,
@@ -515,8 +555,8 @@ export async function buildUnwindWriterUnsoldWithLoanRepayment(
515
555
  : 0n;
516
556
 
517
557
  invariant(
518
- preflight.canRepayFully,
519
- `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)}`
558
+ preflight.canRepayRequestedSlice,
559
+ `Unwind cannot repay the requested slice: 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)}`
520
560
  );
521
561
  if (isWsolPath && lamportsToWrap > 0n && !params.includeWrapForShortfall) {
522
562
  invariant(
@@ -87,6 +87,8 @@ export interface UnwindPreflightSummary {
87
87
 
88
88
  export interface UnwindPreflightResult {
89
89
  canUnwind: boolean;
90
+ canRepayRequestedSlice: boolean;
91
+ /** @deprecated Use canRepayRequestedSlice. */
90
92
  canRepayFully: boolean;
91
93
  reason?: string;
92
94
  writerPositionAddress: string;
@@ -145,6 +147,7 @@ export async function preflightUnwindWriterUnsold(
145
147
  if (unwindQty <= 0n) {
146
148
  return {
147
149
  canUnwind: false,
150
+ canRepayRequestedSlice: false,
148
151
  canRepayFully: false,
149
152
  reason: "unwindQty must be > 0",
150
153
  writerPositionAddress: String(writerPositionAddress),
@@ -178,6 +181,7 @@ export async function preflightUnwindWriterUnsold(
178
181
  if (unwindQty > unsoldQty) {
179
182
  return {
180
183
  canUnwind: false,
184
+ canRepayRequestedSlice: false,
181
185
  canRepayFully: false,
182
186
  reason: "unwindQty exceeds writer unsold quantity",
183
187
  writerPositionAddress: String(writerPositionAddress),
@@ -211,7 +215,7 @@ export async function preflightUnwindWriterUnsold(
211
215
 
212
216
  const slotNow = toBigInt(currentSlot);
213
217
  const protocolFeeBps = BigInt(vault.protocolFeeBps);
214
- const slotsPerYear = 63_072_000n;
218
+ const slotsPerYear = 78_840_000n;
215
219
  const loanBreakdown: Array<UnwindLoanBreakdown> = [];
216
220
 
217
221
  for (const loan of loans) {
@@ -267,20 +271,20 @@ export async function preflightUnwindWriterUnsold(
267
271
 
268
272
  // Calculate proportional obligations for partial unwinds
269
273
  const writtenQty = toBigInt(writerPosition.writtenQty);
270
- const unwindRatio = writtenQty > 0n ? (unwindQty * 1_000_000n) / writtenQty : 0n; // Basis points precision
271
- const unwindRatioDecimal = Number(unwindRatio) / 1_000_000; // Convert to decimal
272
-
273
- // Proportional obligations (for partial unwind logic)
274
274
  const proportionalPrincipal = writtenQty > 0n ? (totals.principal * unwindQty) / writtenQty : 0n;
275
275
  const proportionalInterest = writtenQty > 0n ? (totals.interest * unwindQty) / writtenQty : 0n;
276
276
  const proportionalProtocolFees = writtenQty > 0n ? (totals.fees * unwindQty) / writtenQty : 0n;
277
277
  const proportionalTotalOwed = proportionalPrincipal + proportionalInterest + proportionalProtocolFees;
278
278
 
279
- // Collateral return calculation (proportional share minus proportional obligations)
279
+ // On-chain unwind repays from collateral vault first, then optional wallet fallback.
280
+ // The collateral share returned to the writer is therefore reduced only by the
281
+ // amount actually sourced from the collateral vault, not by the entire debt slice.
280
282
  const collateralDeposited = toBigInt(writerPosition.collateralDeposited);
281
283
  const proportionalCollateralShare = writtenQty > 0n ? (collateralDeposited * unwindQty) / writtenQty : 0n;
282
- const returnableCollateral = proportionalCollateralShare > proportionalTotalOwed
283
- ? proportionalCollateralShare - proportionalTotalOwed
284
+ const collateralUsedForRepay =
285
+ proportionalTotalOwed > collateralVaultAvailable ? collateralVaultAvailable : proportionalTotalOwed;
286
+ const returnableCollateral = proportionalCollateralShare > collateralUsedForRepay
287
+ ? proportionalCollateralShare - collateralUsedForRepay
284
288
  : 0n;
285
289
 
286
290
  // Calculate shortfall against proportional obligations
@@ -295,14 +299,13 @@ export async function preflightUnwindWriterUnsold(
295
299
  const effectiveShortfall =
296
300
  proportionalTotalOwed > effectiveTotalAvailable ? proportionalTotalOwed - effectiveTotalAvailable : 0n;
297
301
 
298
- // For top-up UX: explicit collateral vault shortfall
299
- const collateralVaultShortfall = returnableCollateral > collateralVaultAvailable
300
- ? returnableCollateral - collateralVaultAvailable
301
- : 0n;
302
- const needsWalletTopUp = collateralVaultShortfall > 0n && walletFallbackAvailable < collateralVaultShortfall;
302
+ // For top-up UX, surface the wallet contribution required after collateral-vault-first repayment.
303
+ const collateralVaultShortfall = walletFallbackRequired;
304
+ const needsWalletTopUp = walletFallbackRequired > walletFallbackAvailable;
303
305
 
304
306
  return {
305
307
  canUnwind: true,
308
+ canRepayRequestedSlice: effectiveShortfall === 0n,
306
309
  canRepayFully: effectiveShortfall === 0n,
307
310
  reason:
308
311
  effectiveShortfall === 0n