@epicentral/sos-sdk 0.11.2-beta → 0.12.1-beta

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/long/builders.ts CHANGED
@@ -10,7 +10,8 @@ import {
10
10
  deriveAssociatedTokenAddress,
11
11
  deriveBuyerPositionPda,
12
12
  } from "../accounts/pdas";
13
- import { assertNonNegativeAmount, assertPositiveAmount } from "../shared/amounts";
13
+ import { assertNonNegativeAmount, assertPositiveAmount, getPremiumMint } from "../shared/amounts";
14
+ import { fetchTokenAccountMint } from "../shared/balances";
14
15
  import { invariant } from "../shared/errors";
15
16
  import {
16
17
  appendRemainingAccounts,
@@ -47,6 +48,8 @@ export interface BuildBuyFromPoolParams {
47
48
  /** Program `switchboard_queue` — use {@link getDefaultSwitchboardQueueAddress} for the cluster. */
48
49
  switchboardQueue: AddressLike;
49
50
  buyer: AddressLike;
51
+ /** Premium payment mint — must match on-chain premium_vault.mint. */
52
+ paymentMint: AddressLike;
50
53
  buyerPaymentAccount: AddressLike;
51
54
  escrowLongAccount: AddressLike;
52
55
  premiumVault: AddressLike;
@@ -57,6 +60,29 @@ export interface BuildBuyFromPoolParams {
57
60
  remainingAccounts?: RemainingAccountInput[];
58
61
  }
59
62
 
63
+ async function resolveBuyPaymentAccounts(
64
+ params: {
65
+ rpc: KitRpc;
66
+ buyer: AddressLike;
67
+ premiumVault: AddressLike;
68
+ underlyingMint: AddressLike;
69
+ collateralMint?: AddressLike;
70
+ paymentMint?: AddressLike;
71
+ buyerPaymentAccount?: AddressLike;
72
+ }
73
+ ): Promise<{ paymentMint: AddressLike; buyerPaymentAccount: AddressLike }> {
74
+ const onChainPremiumMint = await fetchTokenAccountMint(params.rpc, params.premiumVault);
75
+ const paymentMint = params.paymentMint ?? onChainPremiumMint;
76
+ invariant(
77
+ toAddress(paymentMint) === toAddress(onChainPremiumMint),
78
+ "paymentMint must match on-chain premium_vault.mint"
79
+ );
80
+ const buyerPaymentAccount =
81
+ params.buyerPaymentAccount ??
82
+ (await deriveAssociatedTokenAddress(params.buyer, paymentMint));
83
+ return { paymentMint, buyerPaymentAccount };
84
+ }
85
+
60
86
  export interface BuildCloseLongToPoolParams {
61
87
  optionPool: AddressLike;
62
88
  optionAccount: AddressLike;
@@ -69,7 +95,11 @@ export interface BuildCloseLongToPoolParams {
69
95
  switchboardQueue: AddressLike;
70
96
  buyer: AddressLike;
71
97
  buyerLongAccount: AddressLike;
98
+ /** Settlement-mint ATA (collateral pool mint). */
72
99
  buyerPayoutAccount: AddressLike;
100
+ /** Underlying-mint ATA for premium-vault leg (same as buyerPayout when physical). */
101
+ buyerUnderlyingPayoutAccount: AddressLike;
102
+ collateralMint?: AddressLike;
73
103
  collateralVault: AddressLike;
74
104
  quantity: bigint | number;
75
105
  minPayoutAmount: bigint | number;
@@ -106,6 +136,7 @@ export async function buildBuyFromPoolInstruction(
106
136
  ? toAddress(params.buyerOptionAccount)
107
137
  : undefined,
108
138
  buyerPaymentAccount: toAddress(params.buyerPaymentAccount),
139
+ paymentMint: toAddress(params.paymentMint),
109
140
  escrowLongAccount: toAddress(params.escrowLongAccount),
110
141
  premiumVault: toAddress(params.premiumVault),
111
142
  quantity: params.quantity,
@@ -121,13 +152,13 @@ export async function buildBuyFromPoolInstruction(
121
152
  * first-time buyers succeed without a separate setup step.
122
153
  */
123
154
  export async function buildBuyFromPoolTransaction(
124
- params: BuildBuyFromPoolParams
155
+ params: BuildBuyFromPoolParams & { createPaymentAta?: boolean }
125
156
  ): Promise<BuiltTransaction> {
126
157
  const buyerOptionAccountAddress = params.buyerOptionAccount
127
158
  ? toAddress(params.buyerOptionAccount)
128
159
  : await deriveAssociatedTokenAddress(params.buyer, params.longMint);
129
160
 
130
- const createAtaIx =
161
+ const createLongAtaIx =
131
162
  await getCreateAssociatedTokenIdempotentInstructionWithAddress(
132
163
  params.buyer,
133
164
  params.buyer,
@@ -135,8 +166,22 @@ export async function buildBuyFromPoolTransaction(
135
166
  buyerOptionAccountAddress
136
167
  );
137
168
 
169
+ const instructions: Instruction<string>[] = [createLongAtaIx];
170
+
171
+ if (params.createPaymentAta !== false) {
172
+ const createPaymentAtaIx =
173
+ await getCreateAssociatedTokenIdempotentInstructionWithAddress(
174
+ params.buyer,
175
+ params.buyer,
176
+ params.paymentMint,
177
+ toAddress(params.buyerPaymentAccount)
178
+ );
179
+ instructions.push(createPaymentAtaIx);
180
+ }
181
+
138
182
  const buyFromPoolIx = await buildBuyFromPoolInstruction(params);
139
- return { instructions: [createAtaIx, buyFromPoolIx] };
183
+ instructions.push(buyFromPoolIx);
184
+ return { instructions };
140
185
  }
141
186
 
142
187
  export interface BuildBuyFromPoolTransactionWithDerivationParams {
@@ -145,7 +190,10 @@ export interface BuildBuyFromPoolTransactionWithDerivationParams {
145
190
  strikePrice: number;
146
191
  expirationDate: bigint | number;
147
192
  buyer: AddressLike;
148
- buyerPaymentAccount: AddressLike;
193
+ /** Optional — derived from on-chain premium_vault.mint when omitted. */
194
+ buyerPaymentAccount?: AddressLike;
195
+ /** Optional — derived from on-chain premium_vault.mint when omitted. */
196
+ paymentMint?: AddressLike;
149
197
  /** When `disableSwitchboardCrank` is true, optional explicit queue (else devnet/mainnet default). */
150
198
  switchboardQueue?: AddressLike;
151
199
  quantity: bigint | number;
@@ -231,7 +279,7 @@ export async function buildBuyFromPoolTransactionWithDerivation(
231
279
  "Option pool must exist; ensure rpc is provided and pool is initialized."
232
280
  );
233
281
 
234
- const [buyerPosition, buyerOptionAccount] = await Promise.all([
282
+ const [buyerPosition, buyerOptionAccount, paymentAccounts] = await Promise.all([
235
283
  params.buyerPosition
236
284
  ? Promise.resolve(params.buyerPosition)
237
285
  : deriveBuyerPositionPda(
@@ -242,8 +290,35 @@ export async function buildBuyFromPoolTransactionWithDerivation(
242
290
  params.buyerOptionAccount
243
291
  ? Promise.resolve(params.buyerOptionAccount)
244
292
  : deriveAssociatedTokenAddress(params.buyer, resolved.longMint),
293
+ resolveBuyPaymentAccounts({
294
+ rpc: params.rpc,
295
+ buyer: params.buyer,
296
+ premiumVault: resolved.premiumVault!,
297
+ underlyingMint: resolved.underlyingMint!,
298
+ collateralMint: resolved.collateralPoolData?.collateralMint,
299
+ paymentMint: params.paymentMint,
300
+ buyerPaymentAccount: params.buyerPaymentAccount,
301
+ }),
245
302
  ]);
246
303
 
304
+ const buyParams = {
305
+ optionPool: resolved.optionPool,
306
+ optionAccount: resolved.optionAccount,
307
+ longMint: resolved.longMint,
308
+ underlyingMint: resolved.underlyingMint!,
309
+ marketData: resolved.marketData,
310
+ buyer: params.buyer,
311
+ paymentMint: paymentAccounts.paymentMint,
312
+ buyerPaymentAccount: paymentAccounts.buyerPaymentAccount,
313
+ escrowLongAccount: resolved.escrowLongAccount!,
314
+ premiumVault: resolved.premiumVault!,
315
+ quantity: params.quantity,
316
+ premiumAmount: params.premiumAmount,
317
+ buyerPosition,
318
+ buyerOptionAccount,
319
+ remainingAccounts: params.remainingAccounts,
320
+ };
321
+
247
322
  const marketDataAccount = await fetchMarketDataAccount(params.rpc, resolved.marketData);
248
323
  invariant(
249
324
  !!marketDataAccount,
@@ -259,21 +334,8 @@ export async function buildBuyFromPoolTransactionWithDerivation(
259
334
  ? toAddress(params.switchboardQueue)
260
335
  : getDefaultSwitchboardQueueAddress(network);
261
336
  return buildBuyFromPoolTransaction({
262
- optionPool: resolved.optionPool,
263
- optionAccount: resolved.optionAccount,
264
- longMint: resolved.longMint,
265
- underlyingMint: resolved.underlyingMint!,
266
- marketData: resolved.marketData,
337
+ ...buyParams,
267
338
  switchboardQueue,
268
- buyer: params.buyer,
269
- buyerPaymentAccount: params.buyerPaymentAccount,
270
- escrowLongAccount: resolved.escrowLongAccount!,
271
- premiumVault: resolved.premiumVault!,
272
- quantity: params.quantity,
273
- premiumAmount: params.premiumAmount,
274
- buyerPosition,
275
- buyerOptionAccount,
276
- remainingAccounts: params.remainingAccounts,
277
339
  });
278
340
  }
279
341
 
@@ -292,21 +354,8 @@ export async function buildBuyFromPoolTransactionWithDerivation(
292
354
  });
293
355
 
294
356
  const actionTx = await buildBuyFromPoolTransaction({
295
- optionPool: resolved.optionPool,
296
- optionAccount: resolved.optionAccount,
297
- longMint: resolved.longMint,
298
- underlyingMint: resolved.underlyingMint!,
299
- marketData: resolved.marketData,
357
+ ...buyParams,
300
358
  switchboardQueue: getDefaultSwitchboardQueueAddress(network),
301
- buyer: params.buyer,
302
- buyerPaymentAccount: params.buyerPaymentAccount,
303
- escrowLongAccount: resolved.escrowLongAccount!,
304
- premiumVault: resolved.premiumVault!,
305
- quantity: params.quantity,
306
- premiumAmount: params.premiumAmount,
307
- buyerPosition,
308
- buyerOptionAccount,
309
- remainingAccounts: params.remainingAccounts,
310
359
  });
311
360
 
312
361
  return prependSwitchboardQuote(quote, actionTx);
@@ -341,7 +390,7 @@ export async function buildBuyFromPoolMarketOrderTransactionWithDerivation(
341
390
  rpc: params.rpc,
342
391
  });
343
392
 
344
- const [refetchedPool, remainingAccounts, buyerPosition, buyerOptionAccount] =
393
+ const [refetchedPool, remainingAccounts, buyerPosition, buyerOptionAccount, paymentAccounts] =
345
394
  await Promise.all([
346
395
  fetchOptionPool(params.rpc, resolved.optionPool),
347
396
  getBuyFromPoolRemainingAccounts(params.rpc, resolved.optionPool, params.programId),
@@ -355,6 +404,15 @@ export async function buildBuyFromPoolMarketOrderTransactionWithDerivation(
355
404
  params.buyerOptionAccount
356
405
  ? Promise.resolve(params.buyerOptionAccount)
357
406
  : deriveAssociatedTokenAddress(params.buyer, resolved.longMint),
407
+ resolveBuyPaymentAccounts({
408
+ rpc: params.rpc,
409
+ buyer: params.buyer,
410
+ premiumVault: resolved.premiumVault!,
411
+ underlyingMint: resolved.underlyingMint!,
412
+ collateralMint: resolved.collateralPoolData?.collateralMint,
413
+ paymentMint: params.paymentMint,
414
+ buyerPaymentAccount: params.buyerPaymentAccount,
415
+ }),
358
416
  ]);
359
417
 
360
418
  invariant(
@@ -388,18 +446,36 @@ export async function buildBuyFromPoolMarketOrderTransactionWithDerivation(
388
446
  const onchainPad = (q: bigint) =>
389
447
  (q * BigInt(POOL_BUY_MAX_PREMIUM_ONCHAIN_PAD_BPS)) / 10_000n;
390
448
  const slippageBuffer = hasExplicitSlippageBuffer
391
- ? normalizeMarketOrderSlippageBuffer(params, refetchedPool.underlyingMint) +
449
+ ? normalizeMarketOrderSlippageBuffer(params, paymentAccounts.paymentMint) +
392
450
  onchainPad(quotePremium)
393
451
  : globalTradeConfig.slippageBps !== undefined
394
452
  ? applySlippageBps(
395
453
  quotePremium,
396
454
  globalTradeConfig.slippageBps + POOL_BUY_MAX_PREMIUM_ONCHAIN_PAD_BPS
397
455
  ) - quotePremium
398
- : normalizeMarketOrderSlippageBuffer(params, refetchedPool.underlyingMint) +
456
+ : normalizeMarketOrderSlippageBuffer(params, paymentAccounts.paymentMint) +
399
457
  onchainPad(quotePremium);
400
458
  const maxPremiumAmount = quotePremium + slippageBuffer;
401
459
  assertPositiveAmount(maxPremiumAmount, "maxPremiumAmount");
402
460
 
461
+ const marketBuyParams = {
462
+ optionPool: resolved.optionPool,
463
+ optionAccount: resolved.optionAccount,
464
+ longMint: resolved.longMint,
465
+ underlyingMint: refetchedPool.underlyingMint,
466
+ marketData: resolved.marketData,
467
+ buyer: params.buyer,
468
+ paymentMint: paymentAccounts.paymentMint,
469
+ buyerPaymentAccount: paymentAccounts.buyerPaymentAccount,
470
+ escrowLongAccount: refetchedPool.escrowLongAccount,
471
+ premiumVault: refetchedPool.premiumVault,
472
+ quantity: params.quantity,
473
+ premiumAmount: maxPremiumAmount,
474
+ buyerPosition,
475
+ buyerOptionAccount,
476
+ remainingAccounts,
477
+ };
478
+
403
479
  const marketDataAccount = await fetchMarketDataAccount(params.rpc, resolved.marketData);
404
480
  invariant(
405
481
  !!marketDataAccount,
@@ -415,21 +491,8 @@ export async function buildBuyFromPoolMarketOrderTransactionWithDerivation(
415
491
  ? toAddress(params.switchboardQueue)
416
492
  : getDefaultSwitchboardQueueAddress(network);
417
493
  return buildBuyFromPoolTransaction({
418
- optionPool: resolved.optionPool,
419
- optionAccount: resolved.optionAccount,
420
- longMint: resolved.longMint,
421
- underlyingMint: refetchedPool.underlyingMint,
422
- marketData: resolved.marketData,
494
+ ...marketBuyParams,
423
495
  switchboardQueue,
424
- buyer: params.buyer,
425
- buyerPaymentAccount: params.buyerPaymentAccount,
426
- escrowLongAccount: refetchedPool.escrowLongAccount,
427
- premiumVault: refetchedPool.premiumVault,
428
- quantity: params.quantity,
429
- premiumAmount: maxPremiumAmount,
430
- buyerPosition,
431
- buyerOptionAccount,
432
- remainingAccounts,
433
496
  });
434
497
  }
435
498
 
@@ -448,21 +511,8 @@ export async function buildBuyFromPoolMarketOrderTransactionWithDerivation(
448
511
  });
449
512
 
450
513
  const actionTx = await buildBuyFromPoolTransaction({
451
- optionPool: resolved.optionPool,
452
- optionAccount: resolved.optionAccount,
453
- longMint: resolved.longMint,
454
- underlyingMint: refetchedPool.underlyingMint,
455
- marketData: resolved.marketData,
514
+ ...marketBuyParams,
456
515
  switchboardQueue: getDefaultSwitchboardQueueAddress(network),
457
- buyer: params.buyer,
458
- buyerPaymentAccount: params.buyerPaymentAccount,
459
- escrowLongAccount: refetchedPool.escrowLongAccount,
460
- premiumVault: refetchedPool.premiumVault,
461
- quantity: params.quantity,
462
- premiumAmount: maxPremiumAmount,
463
- buyerPosition,
464
- buyerOptionAccount,
465
- remainingAccounts,
466
516
  });
467
517
 
468
518
  return prependSwitchboardQuote(quote, actionTx);
@@ -477,11 +527,14 @@ export async function buildCloseLongToPoolInstruction(
477
527
  "minPayoutAmount must be greater than or equal to zero."
478
528
  );
479
529
 
530
+ const collateralMint = params.collateralMint ?? params.underlyingMint;
531
+
480
532
  const kitInstruction = await getCloseLongToPoolInstructionAsync({
481
533
  optionPool: toAddress(params.optionPool),
482
534
  optionAccount: toAddress(params.optionAccount),
483
535
  collateralPool: toAddress(params.collateralPool),
484
536
  underlyingMint: toAddress(params.underlyingMint),
537
+ collateralMint: toAddress(collateralMint),
485
538
  longMint: toAddress(params.longMint),
486
539
  escrowLongAccount: toAddress(params.escrowLongAccount),
487
540
  premiumVault: toAddress(params.premiumVault),
@@ -490,6 +543,7 @@ export async function buildCloseLongToPoolInstruction(
490
543
  buyer: toAddress(params.buyer) as any,
491
544
  buyerLongAccount: toAddress(params.buyerLongAccount),
492
545
  buyerPayoutAccount: toAddress(params.buyerPayoutAccount),
546
+ buyerUnderlyingPayoutAccount: toAddress(params.buyerUnderlyingPayoutAccount),
493
547
  collateralVault: toAddress(params.collateralVault),
494
548
  buyerPosition: params.buyerPosition ? toAddress(params.buyerPosition) : undefined,
495
549
  quantity: params.quantity,
@@ -521,7 +575,7 @@ export async function buildCloseLongToPoolTransaction(
521
575
  if (shouldUnwrapPayout) {
522
576
  instructions.push(
523
577
  getCloseAccountInstruction(
524
- params.buyerPayoutAccount,
578
+ params.buyerUnderlyingPayoutAccount,
525
579
  params.buyer,
526
580
  params.buyer
527
581
  )
@@ -538,7 +592,10 @@ export interface BuildCloseLongToPoolTransactionWithDerivationParams {
538
592
  expirationDate: bigint | number;
539
593
  buyer: AddressLike;
540
594
  buyerLongAccount: AddressLike;
541
- buyerPayoutAccount: AddressLike;
595
+ /** Optional — derived from collateral pool mint when omitted. */
596
+ buyerPayoutAccount?: AddressLike;
597
+ /** Optional — derived from underlying mint when omitted. */
598
+ buyerUnderlyingPayoutAccount?: AddressLike;
542
599
  switchboardQueue?: AddressLike;
543
600
  quantity: bigint | number;
544
601
  minPayoutAmount: bigint | number;
@@ -595,6 +652,17 @@ export async function buildCloseLongToPoolTransactionWithDerivation(
595
652
  params.programId
596
653
  ))[0];
597
654
 
655
+ const collateralMint =
656
+ resolved.collateralPoolData?.collateralMint ?? resolved.underlyingMint!;
657
+ const [buyerPayoutAccount, buyerUnderlyingPayoutAccount] = await Promise.all([
658
+ params.buyerPayoutAccount
659
+ ? Promise.resolve(params.buyerPayoutAccount)
660
+ : deriveAssociatedTokenAddress(params.buyer, collateralMint),
661
+ params.buyerUnderlyingPayoutAccount
662
+ ? Promise.resolve(params.buyerUnderlyingPayoutAccount)
663
+ : deriveAssociatedTokenAddress(params.buyer, resolved.underlyingMint!),
664
+ ]);
665
+
598
666
  const isWsolUnderlying =
599
667
  toAddress(resolved.underlyingMint!) === toAddress(NATIVE_MINT);
600
668
  const closeLongTokenAccount =
@@ -632,6 +700,7 @@ export async function buildCloseLongToPoolTransactionWithDerivation(
632
700
  optionAccount: resolved.optionAccount,
633
701
  collateralPool: resolved.collateralPool,
634
702
  underlyingMint: resolved.underlyingMint!,
703
+ collateralMint,
635
704
  longMint: resolved.longMint,
636
705
  escrowLongAccount: resolved.escrowLongAccount!,
637
706
  premiumVault: resolved.premiumVault!,
@@ -639,7 +708,8 @@ export async function buildCloseLongToPoolTransactionWithDerivation(
639
708
  switchboardQueue,
640
709
  buyer: params.buyer,
641
710
  buyerLongAccount: params.buyerLongAccount,
642
- buyerPayoutAccount: params.buyerPayoutAccount,
711
+ buyerPayoutAccount,
712
+ buyerUnderlyingPayoutAccount,
643
713
  collateralVault: resolved.collateralVault!,
644
714
  quantity: params.quantity,
645
715
  minPayoutAmount: params.minPayoutAmount,
@@ -669,6 +739,7 @@ export async function buildCloseLongToPoolTransactionWithDerivation(
669
739
  optionAccount: resolved.optionAccount,
670
740
  collateralPool: resolved.collateralPool,
671
741
  underlyingMint: resolved.underlyingMint!,
742
+ collateralMint,
672
743
  longMint: resolved.longMint,
673
744
  escrowLongAccount: resolved.escrowLongAccount!,
674
745
  premiumVault: resolved.premiumVault!,
@@ -676,7 +747,8 @@ export async function buildCloseLongToPoolTransactionWithDerivation(
676
747
  switchboardQueue: getDefaultSwitchboardQueueAddress(network),
677
748
  buyer: params.buyer,
678
749
  buyerLongAccount: params.buyerLongAccount,
679
- buyerPayoutAccount: params.buyerPayoutAccount,
750
+ buyerPayoutAccount,
751
+ buyerUnderlyingPayoutAccount,
680
752
  collateralVault: resolved.collateralVault!,
681
753
  quantity: params.quantity,
682
754
  minPayoutAmount: params.minPayoutAmount,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@epicentral/sos-sdk",
3
- "version": "0.11.2-beta",
3
+ "version": "0.12.1-beta",
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/shared/amounts.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import Decimal from "decimal.js";
2
+ import type { AddressLike } from "../client/types";
2
3
  import { SdkValidationError } from "./errors";
3
4
 
4
5
  export function toBaseUnits(amount: Decimal.Value, decimals: number): bigint {
@@ -24,30 +25,90 @@ export function assertNonNegativeAmount(value: bigint | number, label: string):
24
25
  }
25
26
  }
26
27
 
28
+ export type SettlementMode = "physical" | "cash";
29
+
27
30
  /**
28
- * Calculate required collateral for option position in token base units
29
- * Matches on-chain formula: ((qty / 1_000_000) * 100 * strike) / spot * 10^decimals
30
- *
31
- * @param quantity - Option quantity in base units (1 contract = 1_000_000)
32
- * @param strikePrice - Strike price in USD
33
- * @param spotPrice - Current spot price of underlying in USD (from oracle)
34
- * @param tokenDecimals - Number of decimals for the underlying token (e.g., 9 for SOL)
35
- * @returns Required collateral in token base units
31
+ * Calculate required collateral in **collateral mint** base units.
32
+ * Matches on-chain `Vault::calculate_required_collateral`:
33
+ * `((qty / 1_000_000) * 100 * strike) / conversionPrice * 10^decimals`.
34
+ *
35
+ * - **Physical:** pass underlying oracle spot + underlying/collateral decimals.
36
+ * - **Cash** (e.g. USDC backing SOL): pass collateral USD price (~1 for stables) + collateral decimals.
36
37
  */
37
38
  export function calculateRequiredCollateral(
38
39
  quantity: bigint | number,
39
40
  strikePrice: number,
40
- spotPrice: number,
41
- tokenDecimals: number
41
+ conversionPrice: number,
42
+ tokenDecimals: number,
43
+ settlementMode: SettlementMode = "physical"
42
44
  ): number {
43
- // Convert base units to contract count
45
+ if (conversionPrice <= 0 || !Number.isFinite(conversionPrice)) {
46
+ return Number.POSITIVE_INFINITY;
47
+ }
48
+
44
49
  const contracts = Number(quantity) / 1_000_000;
45
- const contractSize = 100; // 1 contract = 100 units of underlying
46
-
47
- // USD value needed for collateral
50
+ const contractSize = 100;
48
51
  const usdRequired = contracts * contractSize * strikePrice;
49
-
50
- // Convert USD to token base units
51
52
  const baseUnits = 10 ** tokenDecimals;
52
- return (usdRequired / spotPrice) * baseUnits;
53
+
54
+ if (settlementMode === "cash") {
55
+ return usdRequired * baseUnits;
56
+ }
57
+
58
+ return (usdRequired / conversionPrice) * baseUnits;
59
+ }
60
+
61
+ /** @deprecated Use `calculateRequiredCollateral` with explicit settlement mode. */
62
+ export function calculateRequiredCollateralLegacy(
63
+ quantity: bigint | number,
64
+ strikePrice: number,
65
+ spotPrice: number,
66
+ tokenDecimals: number
67
+ ): number {
68
+ return calculateRequiredCollateral(
69
+ quantity,
70
+ strikePrice,
71
+ spotPrice,
72
+ tokenDecimals,
73
+ "physical"
74
+ );
75
+ }
76
+
77
+ /**
78
+ * Premium payment mint for a pool: underlying when physical, collateral when cash.
79
+ * For **existing** pools, prefer on-chain `premium_vault.mint` (legacy cash may differ).
80
+ */
81
+ export function getPremiumMint(
82
+ underlyingMint: AddressLike,
83
+ collateralMint: AddressLike
84
+ ): string {
85
+ const underlying = String(underlyingMint);
86
+ const collateral = String(collateralMint);
87
+ return underlying === collateral ? underlying : collateral;
88
+ }
89
+
90
+ /**
91
+ * Convert BS premium USD to payment-mint base units.
92
+ * - **Cash:** `premiumUsd × 10^decimals` (~$1 stable collateral)
93
+ * - **Physical:** `(premiumUsd / underlyingSpot) × 10^decimals`
94
+ */
95
+ export function convertPremiumUsdToBaseUnits(
96
+ premiumUsd: number,
97
+ settlementMode: SettlementMode,
98
+ underlyingSpot: number,
99
+ tokenDecimals: number
100
+ ): bigint {
101
+ if (!Number.isFinite(premiumUsd) || premiumUsd <= 0) {
102
+ return 0n;
103
+ }
104
+ const scale = 10 ** tokenDecimals;
105
+ if (settlementMode === "cash") {
106
+ return BigInt(Math.ceil(premiumUsd * scale - Number.EPSILON));
107
+ }
108
+ if (!Number.isFinite(underlyingSpot) || underlyingSpot <= 0) {
109
+ return 0n;
110
+ }
111
+ return BigInt(
112
+ Math.ceil((premiumUsd / underlyingSpot) * scale - Number.EPSILON)
113
+ );
53
114
  }
@@ -1,27 +1,62 @@
1
1
  import type { Address } from "@solana/kit";
2
+ import { getAddressDecoder } from "@solana/kit";
2
3
  import { deriveAssociatedTokenAddress } from "../accounts/pdas";
3
4
  import { toAddress } from "../client/program";
4
5
  import type { AddressLike, KitRpc } from "../client/types";
5
6
  import { NATIVE_MINT } from "../wsol/instructions";
6
7
 
7
- /** SPL Token account data: amount field offset (u64 LE). */
8
+ /** SPL Token account data: mint at offset 0 (32 bytes); amount at offset 64 (u64 LE). */
9
+ const TOKEN_ACCOUNT_MINT_OFFSET = 0;
8
10
  const TOKEN_ACCOUNT_AMOUNT_OFFSET = 64;
9
11
 
12
+ function decodeTokenAccountData(b64: string): Uint8Array {
13
+ const binary = atob(b64);
14
+ const data = new Uint8Array(binary.length);
15
+ for (let i = 0; i < binary.length; i++) data[i] = binary.charCodeAt(i);
16
+ return data;
17
+ }
18
+
19
+ function decodeTokenAccountMint(data: Uint8Array): Address | null {
20
+ if (data.length < TOKEN_ACCOUNT_MINT_OFFSET + 32) return null;
21
+ return getAddressDecoder().decode(
22
+ data.subarray(TOKEN_ACCOUNT_MINT_OFFSET, TOKEN_ACCOUNT_MINT_OFFSET + 32)
23
+ );
24
+ }
25
+
10
26
  function decodeTokenAccountAmount(data: Uint8Array): bigint {
11
27
  if (data.length < TOKEN_ACCOUNT_AMOUNT_OFFSET + 8) return BigInt(0);
12
28
  const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
13
29
  return view.getBigUint64(TOKEN_ACCOUNT_AMOUNT_OFFSET, true);
14
30
  }
15
31
 
16
- async function fetchTokenAccountBalance(rpc: KitRpc, ata: Address): Promise<bigint> {
32
+ async function fetchTokenAccountData(rpc: KitRpc, ata: Address): Promise<Uint8Array | null> {
17
33
  const response = await rpc.getAccountInfo(ata, { encoding: "base64" }).send();
18
34
  const accountInfo = response.value;
19
- if (!accountInfo) return BigInt(0);
35
+ if (!accountInfo) return null;
20
36
  const [b64] = accountInfo.data;
21
- if (!b64) return BigInt(0);
22
- const binary = atob(b64);
23
- const data = new Uint8Array(binary.length);
24
- for (let i = 0; i < binary.length; i++) data[i] = binary.charCodeAt(i);
37
+ if (!b64) return null;
38
+ return decodeTokenAccountData(b64);
39
+ }
40
+
41
+ /** Read the mint pubkey from an SPL token account address. */
42
+ export async function fetchTokenAccountMint(
43
+ rpc: KitRpc,
44
+ tokenAccount: AddressLike
45
+ ): Promise<Address> {
46
+ const data = await fetchTokenAccountData(rpc, toAddress(tokenAccount));
47
+ if (!data) {
48
+ throw new Error(`Token account not found: ${String(tokenAccount)}`);
49
+ }
50
+ const mint = decodeTokenAccountMint(data);
51
+ if (!mint) {
52
+ throw new Error(`Invalid token account data: ${String(tokenAccount)}`);
53
+ }
54
+ return mint;
55
+ }
56
+
57
+ async function fetchTokenAccountBalance(rpc: KitRpc, ata: Address): Promise<bigint> {
58
+ const data = await fetchTokenAccountData(rpc, ata);
59
+ if (!data) return BigInt(0);
25
60
  return decodeTokenAccountAmount(data);
26
61
  }
27
62
 
package/short/builders.ts CHANGED
@@ -666,7 +666,9 @@ export async function buildUnwindWriterUnsoldWithLoanRepayment(
666
666
  "underlyingMint is required; ensure rpc is provided and option pool is initialized, or pass underlyingMint."
667
667
  );
668
668
 
669
- const [vaultPda] = await deriveVaultPda(underlyingMint, params.programId);
669
+ const collateralMint =
670
+ resolved.collateralPoolData?.collateralMint ?? underlyingMint;
671
+ const [vaultPda] = await deriveVaultPda(collateralMint, params.programId);
670
672
  const vaultPdaStr = toAddress(vaultPda);
671
673
 
672
674
  const [writerPositionAddress] = await deriveWriterPositionPda(
@@ -733,9 +735,9 @@ export async function buildUnwindWriterUnsoldWithLoanRepayment(
733
735
  isWritable: true,
734
736
  }));
735
737
 
736
- const omlpVault = await deriveAssociatedTokenAddress(vaultPda, underlyingMint);
738
+ const omlpVault = await deriveAssociatedTokenAddress(vaultPda, collateralMint);
737
739
  const feeWallet = vault
738
- ? await deriveAssociatedTokenAddress(vault.feeWallet, underlyingMint)
740
+ ? await deriveAssociatedTokenAddress(vault.feeWallet, collateralMint)
739
741
  : undefined;
740
742
 
741
743
  // Theta-hedge model: if proportional debt cannot be covered by
@@ -795,10 +797,14 @@ export async function buildUnwindWriterUnsoldTransactionWithDerivation(
795
797
  "Option pool and collateral pool must exist; ensure rpc is provided and pools are initialized."
796
798
  );
797
799
 
800
+ const collateralMint =
801
+ resolved.collateralPoolData?.collateralMint ?? resolved.underlyingMint;
802
+ invariant(!!collateralMint, "collateral mint is required for unwind.");
803
+
798
804
  const [writerShortAccount, writerCollateralAccount, writerPosition] =
799
805
  await Promise.all([
800
806
  deriveAssociatedTokenAddress(params.writer, resolved.shortMint),
801
- deriveAssociatedTokenAddress(params.writer, resolved.underlyingMint),
807
+ deriveAssociatedTokenAddress(params.writer, collateralMint),
802
808
  deriveWriterPositionPda(resolved.optionPool, params.writer, params.programId),
803
809
  ]);
804
810
 
@@ -6,6 +6,7 @@ import { fetchPoolLoansByMaker } from "../accounts/list";
6
6
  import { deriveAssociatedTokenAddress, deriveVaultPda, deriveWriterPositionPda } from "../accounts/pdas";
7
7
  import { resolveOptionAccounts } from "../accounts/resolve-option";
8
8
  import { invariant } from "../shared/errors";
9
+ import { fetchTokenAccountMint } from "../shared/balances";
9
10
 
10
11
  const TOKEN_ACCOUNT_AMOUNT_OFFSET = 64;
11
12
  const BPS_DENOMINATOR = 10_000n;
@@ -455,12 +456,12 @@ export async function preflightUnwindWriterUnsold(
455
456
  const thetaAvailable = computeThetaBalance(writerPosition, optionPoolData.accThetaPerOiFp);
456
457
 
457
458
  // Theta can only offset debt when premium_vault.mint == omlp_vault.mint.
458
- // In OPX, premium_vault.mint == option_pool.underlying_mint (enforced via
459
- // `buyer_payment_account.mint == option_pool.underlying_mint`), and the
460
- // OMLP vault is derived for the collateral mint. They match when the
461
- // collateral pool's collateral_mint equals the option pool's underlying_mint.
459
+ const premiumVaultMint = await fetchTokenAccountMint(
460
+ params.rpc,
461
+ resolved.premiumVault!
462
+ );
462
463
  const premiumMintMatchesCollateralMint =
463
- toAddress(optionPoolData.underlyingMint) === toAddress(collateralPoolData.collateralMint);
464
+ toAddress(premiumVaultMint) === toAddress(collateralPoolData.collateralMint);
464
465
 
465
466
  const thetaToDebt = premiumMintMatchesCollateralMint
466
467
  ? minBigInt(thetaAvailable, proportionalTotalOwed)