@epicentral/sos-sdk 0.12.0-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/README.md CHANGED
@@ -442,7 +442,8 @@ const tx = await buildBuyFromPoolMarketOrderTransactionWithDerivation({
442
442
 
443
443
  ### Buy premium semantics (market orders)
444
444
 
445
- - Premium is paid in the **option pool underlying** token; `buyer_payment_account` must match that mint (see program `buy_from_pool`).
445
+ - Premium is paid in **`premium_vault.mint`** (on-chain source of truth). For **new** pools: underlying when physical (`collateral == underlying`), collateral mint when cash (e.g. USDC). **Legacy** cash pools may still use SOL premium vault — resolve mint from chain before building `buyer_payment_account`.
446
+ - `buyer_payment_account.mint` must match `premium_vault.mint`; pass `payment_mint` to `buy_from_pool`.
446
447
  - `premiumAmount` / `max_premium_amount` is a **max premium cap**, not an exact premium target.
447
448
  - Program computes premium on-chain at execution time and fails with `SlippageToleranceExceeded` if computed premium exceeds the cap.
448
449
  - High-level market builder computes cap as `quotedPremiumTotal + buffer`:
@@ -65,6 +65,7 @@ export type BuyFromPoolInstruction<
65
65
  "Sysvar1nstructions1111111111111111111111111",
66
66
  TAccountBuyerPosition extends string | AccountMeta<string> = string,
67
67
  TAccountBuyerOptionAccount extends string | AccountMeta<string> = string,
68
+ TAccountPaymentMint extends string | AccountMeta<string> = string,
68
69
  TAccountBuyerPaymentAccount extends string | AccountMeta<string> = string,
69
70
  TAccountEscrowLongAccount extends string | AccountMeta<string> = string,
70
71
  TAccountPremiumVault extends string | AccountMeta<string> = string,
@@ -111,6 +112,9 @@ export type BuyFromPoolInstruction<
111
112
  TAccountBuyerOptionAccount extends string
112
113
  ? WritableAccount<TAccountBuyerOptionAccount>
113
114
  : TAccountBuyerOptionAccount,
115
+ TAccountPaymentMint extends string
116
+ ? ReadonlyAccount<TAccountPaymentMint>
117
+ : TAccountPaymentMint,
114
118
  TAccountBuyerPaymentAccount extends string
115
119
  ? WritableAccount<TAccountBuyerPaymentAccount>
116
120
  : TAccountBuyerPaymentAccount,
@@ -191,6 +195,7 @@ export type BuyFromPoolAsyncInput<
191
195
  TAccountInstructionsSysvar extends string = string,
192
196
  TAccountBuyerPosition extends string = string,
193
197
  TAccountBuyerOptionAccount extends string = string,
198
+ TAccountPaymentMint extends string = string,
194
199
  TAccountBuyerPaymentAccount extends string = string,
195
200
  TAccountEscrowLongAccount extends string = string,
196
201
  TAccountPremiumVault extends string = string,
@@ -220,6 +225,8 @@ export type BuyFromPoolAsyncInput<
220
225
  buyerPosition?: Address<TAccountBuyerPosition>;
221
226
  /** Buyer's LONG token account (receives LONG tokens) */
222
227
  buyerOptionAccount?: Address<TAccountBuyerOptionAccount>;
228
+ /** Premium payment mint (must match premium_vault.mint on-chain) */
229
+ paymentMint: Address<TAccountPaymentMint>;
223
230
  /** Buyer's payment account (source of premium) */
224
231
  buyerPaymentAccount: Address<TAccountBuyerPaymentAccount>;
225
232
  /** Pool's LONG escrow (source of LONG tokens) */
@@ -247,6 +254,7 @@ export async function getBuyFromPoolInstructionAsync<
247
254
  TAccountInstructionsSysvar extends string,
248
255
  TAccountBuyerPosition extends string,
249
256
  TAccountBuyerOptionAccount extends string,
257
+ TAccountPaymentMint extends string,
250
258
  TAccountBuyerPaymentAccount extends string,
251
259
  TAccountEscrowLongAccount extends string,
252
260
  TAccountPremiumVault extends string,
@@ -268,6 +276,7 @@ export async function getBuyFromPoolInstructionAsync<
268
276
  TAccountInstructionsSysvar,
269
277
  TAccountBuyerPosition,
270
278
  TAccountBuyerOptionAccount,
279
+ TAccountPaymentMint,
271
280
  TAccountBuyerPaymentAccount,
272
281
  TAccountEscrowLongAccount,
273
282
  TAccountPremiumVault,
@@ -291,6 +300,7 @@ export async function getBuyFromPoolInstructionAsync<
291
300
  TAccountInstructionsSysvar,
292
301
  TAccountBuyerPosition,
293
302
  TAccountBuyerOptionAccount,
303
+ TAccountPaymentMint,
294
304
  TAccountBuyerPaymentAccount,
295
305
  TAccountEscrowLongAccount,
296
306
  TAccountPremiumVault,
@@ -329,6 +339,7 @@ export async function getBuyFromPoolInstructionAsync<
329
339
  value: input.buyerOptionAccount ?? null,
330
340
  isWritable: true,
331
341
  },
342
+ paymentMint: { value: input.paymentMint ?? null, isWritable: false },
332
343
  buyerPaymentAccount: {
333
344
  value: input.buyerPaymentAccount ?? null,
334
345
  isWritable: true,
@@ -433,6 +444,7 @@ export async function getBuyFromPoolInstructionAsync<
433
444
  getAccountMeta(accounts.instructionsSysvar),
434
445
  getAccountMeta(accounts.buyerPosition),
435
446
  getAccountMeta(accounts.buyerOptionAccount),
447
+ getAccountMeta(accounts.paymentMint),
436
448
  getAccountMeta(accounts.buyerPaymentAccount),
437
449
  getAccountMeta(accounts.escrowLongAccount),
438
450
  getAccountMeta(accounts.premiumVault),
@@ -458,6 +470,7 @@ export async function getBuyFromPoolInstructionAsync<
458
470
  TAccountInstructionsSysvar,
459
471
  TAccountBuyerPosition,
460
472
  TAccountBuyerOptionAccount,
473
+ TAccountPaymentMint,
461
474
  TAccountBuyerPaymentAccount,
462
475
  TAccountEscrowLongAccount,
463
476
  TAccountPremiumVault,
@@ -480,6 +493,7 @@ export type BuyFromPoolInput<
480
493
  TAccountInstructionsSysvar extends string = string,
481
494
  TAccountBuyerPosition extends string = string,
482
495
  TAccountBuyerOptionAccount extends string = string,
496
+ TAccountPaymentMint extends string = string,
483
497
  TAccountBuyerPaymentAccount extends string = string,
484
498
  TAccountEscrowLongAccount extends string = string,
485
499
  TAccountPremiumVault extends string = string,
@@ -509,6 +523,8 @@ export type BuyFromPoolInput<
509
523
  buyerPosition: Address<TAccountBuyerPosition>;
510
524
  /** Buyer's LONG token account (receives LONG tokens) */
511
525
  buyerOptionAccount: Address<TAccountBuyerOptionAccount>;
526
+ /** Premium payment mint (must match premium_vault.mint on-chain) */
527
+ paymentMint: Address<TAccountPaymentMint>;
512
528
  /** Buyer's payment account (source of premium) */
513
529
  buyerPaymentAccount: Address<TAccountBuyerPaymentAccount>;
514
530
  /** Pool's LONG escrow (source of LONG tokens) */
@@ -536,6 +552,7 @@ export function getBuyFromPoolInstruction<
536
552
  TAccountInstructionsSysvar extends string,
537
553
  TAccountBuyerPosition extends string,
538
554
  TAccountBuyerOptionAccount extends string,
555
+ TAccountPaymentMint extends string,
539
556
  TAccountBuyerPaymentAccount extends string,
540
557
  TAccountEscrowLongAccount extends string,
541
558
  TAccountPremiumVault extends string,
@@ -557,6 +574,7 @@ export function getBuyFromPoolInstruction<
557
574
  TAccountInstructionsSysvar,
558
575
  TAccountBuyerPosition,
559
576
  TAccountBuyerOptionAccount,
577
+ TAccountPaymentMint,
560
578
  TAccountBuyerPaymentAccount,
561
579
  TAccountEscrowLongAccount,
562
580
  TAccountPremiumVault,
@@ -579,6 +597,7 @@ export function getBuyFromPoolInstruction<
579
597
  TAccountInstructionsSysvar,
580
598
  TAccountBuyerPosition,
581
599
  TAccountBuyerOptionAccount,
600
+ TAccountPaymentMint,
582
601
  TAccountBuyerPaymentAccount,
583
602
  TAccountEscrowLongAccount,
584
603
  TAccountPremiumVault,
@@ -616,6 +635,7 @@ export function getBuyFromPoolInstruction<
616
635
  value: input.buyerOptionAccount ?? null,
617
636
  isWritable: true,
618
637
  },
638
+ paymentMint: { value: input.paymentMint ?? null, isWritable: false },
619
639
  buyerPaymentAccount: {
620
640
  value: input.buyerPaymentAccount ?? null,
621
641
  isWritable: true,
@@ -677,6 +697,7 @@ export function getBuyFromPoolInstruction<
677
697
  getAccountMeta(accounts.instructionsSysvar),
678
698
  getAccountMeta(accounts.buyerPosition),
679
699
  getAccountMeta(accounts.buyerOptionAccount),
700
+ getAccountMeta(accounts.paymentMint),
680
701
  getAccountMeta(accounts.buyerPaymentAccount),
681
702
  getAccountMeta(accounts.escrowLongAccount),
682
703
  getAccountMeta(accounts.premiumVault),
@@ -702,6 +723,7 @@ export function getBuyFromPoolInstruction<
702
723
  TAccountInstructionsSysvar,
703
724
  TAccountBuyerPosition,
704
725
  TAccountBuyerOptionAccount,
726
+ TAccountPaymentMint,
705
727
  TAccountBuyerPaymentAccount,
706
728
  TAccountEscrowLongAccount,
707
729
  TAccountPremiumVault,
@@ -739,18 +761,20 @@ export type ParsedBuyFromPoolInstruction<
739
761
  buyerPosition: TAccountMetas[8];
740
762
  /** Buyer's LONG token account (receives LONG tokens) */
741
763
  buyerOptionAccount: TAccountMetas[9];
764
+ /** Premium payment mint (must match premium_vault.mint on-chain) */
765
+ paymentMint: TAccountMetas[10];
742
766
  /** Buyer's payment account (source of premium) */
743
- buyerPaymentAccount: TAccountMetas[10];
767
+ buyerPaymentAccount: TAccountMetas[11];
744
768
  /** Pool's LONG escrow (source of LONG tokens) */
745
- escrowLongAccount: TAccountMetas[11];
769
+ escrowLongAccount: TAccountMetas[12];
746
770
  /** Pool's premium vault (receives premium) */
747
- premiumVault: TAccountMetas[12];
771
+ premiumVault: TAccountMetas[13];
748
772
  /** Collateral pool (settlement mint for buyer position tracking) */
749
- collateralPool: TAccountMetas[13];
750
- buyer: TAccountMetas[14];
751
- tokenProgram: TAccountMetas[15];
752
- associatedTokenProgram: TAccountMetas[16];
753
- systemProgram: TAccountMetas[17];
773
+ collateralPool: TAccountMetas[14];
774
+ buyer: TAccountMetas[15];
775
+ tokenProgram: TAccountMetas[16];
776
+ associatedTokenProgram: TAccountMetas[17];
777
+ systemProgram: TAccountMetas[18];
754
778
  };
755
779
  data: BuyFromPoolInstructionData;
756
780
  };
@@ -763,7 +787,7 @@ export function parseBuyFromPoolInstruction<
763
787
  InstructionWithAccounts<TAccountMetas> &
764
788
  InstructionWithData<ReadonlyUint8Array>,
765
789
  ): ParsedBuyFromPoolInstruction<TProgram, TAccountMetas> {
766
- if (instruction.accounts.length < 18) {
790
+ if (instruction.accounts.length < 19) {
767
791
  // TODO: Coded error.
768
792
  throw new Error("Not enough accounts");
769
793
  }
@@ -786,6 +810,7 @@ export function parseBuyFromPoolInstruction<
786
810
  instructionsSysvar: getNextAccount(),
787
811
  buyerPosition: getNextAccount(),
788
812
  buyerOptionAccount: getNextAccount(),
813
+ paymentMint: getNextAccount(),
789
814
  buyerPaymentAccount: getNextAccount(),
790
815
  escrowLongAccount: getNextAccount(),
791
816
  premiumVault: getNextAccount(),
@@ -350,7 +350,7 @@ export type OptionMintAsyncInput<
350
350
  optionPool?: Address<TAccountOptionPool>;
351
351
  /** Pool's escrow for holding LONG tokens (for buyers to purchase) */
352
352
  escrowLongAccount?: Address<TAccountEscrowLongAccount>;
353
- /** Pool's vault for collecting premiums (in underlying asset) */
353
+ /** Pool vault for premiums (underlying when physical, collateral mint for cash) */
354
354
  premiumVault?: Address<TAccountPremiumVault>;
355
355
  /** Collateral pool for this option */
356
356
  collateralPool?: Address<TAccountCollateralPool>;
@@ -707,7 +707,7 @@ export async function getOptionMintInstructionAsync<
707
707
  ]),
708
708
  ),
709
709
  getAddressEncoder().encode(
710
- expectAddress(accounts.underlyingMint.value),
710
+ expectAddress(accounts.collateralMint.value),
711
711
  ),
712
712
  ],
713
713
  });
@@ -924,7 +924,7 @@ export type OptionMintInput<
924
924
  optionPool: Address<TAccountOptionPool>;
925
925
  /** Pool's escrow for holding LONG tokens (for buyers to purchase) */
926
926
  escrowLongAccount: Address<TAccountEscrowLongAccount>;
927
- /** Pool's vault for collecting premiums (in underlying asset) */
927
+ /** Pool vault for premiums (underlying when physical, collateral mint for cash) */
928
928
  premiumVault: Address<TAccountPremiumVault>;
929
929
  /** Collateral pool for this option */
930
930
  collateralPool: Address<TAccountCollateralPool>;
@@ -1278,7 +1278,7 @@ export type ParsedOptionMintInstruction<
1278
1278
  optionPool: TAccountMetas[11];
1279
1279
  /** Pool's escrow for holding LONG tokens (for buyers to purchase) */
1280
1280
  escrowLongAccount: TAccountMetas[12];
1281
- /** Pool's vault for collecting premiums (in underlying asset) */
1281
+ /** Pool vault for premiums (underlying when physical, collateral mint for cash) */
1282
1282
  premiumVault: TAccountMetas[13];
1283
1283
  /** Collateral pool for this option */
1284
1284
  collateralPool: TAccountMetas[14];
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;
@@ -110,6 +136,7 @@ export async function buildBuyFromPoolInstruction(
110
136
  ? toAddress(params.buyerOptionAccount)
111
137
  : undefined,
112
138
  buyerPaymentAccount: toAddress(params.buyerPaymentAccount),
139
+ paymentMint: toAddress(params.paymentMint),
113
140
  escrowLongAccount: toAddress(params.escrowLongAccount),
114
141
  premiumVault: toAddress(params.premiumVault),
115
142
  quantity: params.quantity,
@@ -125,13 +152,13 @@ export async function buildBuyFromPoolInstruction(
125
152
  * first-time buyers succeed without a separate setup step.
126
153
  */
127
154
  export async function buildBuyFromPoolTransaction(
128
- params: BuildBuyFromPoolParams
155
+ params: BuildBuyFromPoolParams & { createPaymentAta?: boolean }
129
156
  ): Promise<BuiltTransaction> {
130
157
  const buyerOptionAccountAddress = params.buyerOptionAccount
131
158
  ? toAddress(params.buyerOptionAccount)
132
159
  : await deriveAssociatedTokenAddress(params.buyer, params.longMint);
133
160
 
134
- const createAtaIx =
161
+ const createLongAtaIx =
135
162
  await getCreateAssociatedTokenIdempotentInstructionWithAddress(
136
163
  params.buyer,
137
164
  params.buyer,
@@ -139,8 +166,22 @@ export async function buildBuyFromPoolTransaction(
139
166
  buyerOptionAccountAddress
140
167
  );
141
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
+
142
182
  const buyFromPoolIx = await buildBuyFromPoolInstruction(params);
143
- return { instructions: [createAtaIx, buyFromPoolIx] };
183
+ instructions.push(buyFromPoolIx);
184
+ return { instructions };
144
185
  }
145
186
 
146
187
  export interface BuildBuyFromPoolTransactionWithDerivationParams {
@@ -149,7 +190,10 @@ export interface BuildBuyFromPoolTransactionWithDerivationParams {
149
190
  strikePrice: number;
150
191
  expirationDate: bigint | number;
151
192
  buyer: AddressLike;
152
- 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;
153
197
  /** When `disableSwitchboardCrank` is true, optional explicit queue (else devnet/mainnet default). */
154
198
  switchboardQueue?: AddressLike;
155
199
  quantity: bigint | number;
@@ -235,7 +279,7 @@ export async function buildBuyFromPoolTransactionWithDerivation(
235
279
  "Option pool must exist; ensure rpc is provided and pool is initialized."
236
280
  );
237
281
 
238
- const [buyerPosition, buyerOptionAccount] = await Promise.all([
282
+ const [buyerPosition, buyerOptionAccount, paymentAccounts] = await Promise.all([
239
283
  params.buyerPosition
240
284
  ? Promise.resolve(params.buyerPosition)
241
285
  : deriveBuyerPositionPda(
@@ -246,8 +290,35 @@ export async function buildBuyFromPoolTransactionWithDerivation(
246
290
  params.buyerOptionAccount
247
291
  ? Promise.resolve(params.buyerOptionAccount)
248
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
+ }),
249
302
  ]);
250
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
+
251
322
  const marketDataAccount = await fetchMarketDataAccount(params.rpc, resolved.marketData);
252
323
  invariant(
253
324
  !!marketDataAccount,
@@ -263,21 +334,8 @@ export async function buildBuyFromPoolTransactionWithDerivation(
263
334
  ? toAddress(params.switchboardQueue)
264
335
  : getDefaultSwitchboardQueueAddress(network);
265
336
  return buildBuyFromPoolTransaction({
266
- optionPool: resolved.optionPool,
267
- optionAccount: resolved.optionAccount,
268
- longMint: resolved.longMint,
269
- underlyingMint: resolved.underlyingMint!,
270
- marketData: resolved.marketData,
337
+ ...buyParams,
271
338
  switchboardQueue,
272
- buyer: params.buyer,
273
- buyerPaymentAccount: params.buyerPaymentAccount,
274
- escrowLongAccount: resolved.escrowLongAccount!,
275
- premiumVault: resolved.premiumVault!,
276
- quantity: params.quantity,
277
- premiumAmount: params.premiumAmount,
278
- buyerPosition,
279
- buyerOptionAccount,
280
- remainingAccounts: params.remainingAccounts,
281
339
  });
282
340
  }
283
341
 
@@ -296,21 +354,8 @@ export async function buildBuyFromPoolTransactionWithDerivation(
296
354
  });
297
355
 
298
356
  const actionTx = await buildBuyFromPoolTransaction({
299
- optionPool: resolved.optionPool,
300
- optionAccount: resolved.optionAccount,
301
- longMint: resolved.longMint,
302
- underlyingMint: resolved.underlyingMint!,
303
- marketData: resolved.marketData,
357
+ ...buyParams,
304
358
  switchboardQueue: getDefaultSwitchboardQueueAddress(network),
305
- buyer: params.buyer,
306
- buyerPaymentAccount: params.buyerPaymentAccount,
307
- escrowLongAccount: resolved.escrowLongAccount!,
308
- premiumVault: resolved.premiumVault!,
309
- quantity: params.quantity,
310
- premiumAmount: params.premiumAmount,
311
- buyerPosition,
312
- buyerOptionAccount,
313
- remainingAccounts: params.remainingAccounts,
314
359
  });
315
360
 
316
361
  return prependSwitchboardQuote(quote, actionTx);
@@ -345,7 +390,7 @@ export async function buildBuyFromPoolMarketOrderTransactionWithDerivation(
345
390
  rpc: params.rpc,
346
391
  });
347
392
 
348
- const [refetchedPool, remainingAccounts, buyerPosition, buyerOptionAccount] =
393
+ const [refetchedPool, remainingAccounts, buyerPosition, buyerOptionAccount, paymentAccounts] =
349
394
  await Promise.all([
350
395
  fetchOptionPool(params.rpc, resolved.optionPool),
351
396
  getBuyFromPoolRemainingAccounts(params.rpc, resolved.optionPool, params.programId),
@@ -359,6 +404,15 @@ export async function buildBuyFromPoolMarketOrderTransactionWithDerivation(
359
404
  params.buyerOptionAccount
360
405
  ? Promise.resolve(params.buyerOptionAccount)
361
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
+ }),
362
416
  ]);
363
417
 
364
418
  invariant(
@@ -392,18 +446,36 @@ export async function buildBuyFromPoolMarketOrderTransactionWithDerivation(
392
446
  const onchainPad = (q: bigint) =>
393
447
  (q * BigInt(POOL_BUY_MAX_PREMIUM_ONCHAIN_PAD_BPS)) / 10_000n;
394
448
  const slippageBuffer = hasExplicitSlippageBuffer
395
- ? normalizeMarketOrderSlippageBuffer(params, refetchedPool.underlyingMint) +
449
+ ? normalizeMarketOrderSlippageBuffer(params, paymentAccounts.paymentMint) +
396
450
  onchainPad(quotePremium)
397
451
  : globalTradeConfig.slippageBps !== undefined
398
452
  ? applySlippageBps(
399
453
  quotePremium,
400
454
  globalTradeConfig.slippageBps + POOL_BUY_MAX_PREMIUM_ONCHAIN_PAD_BPS
401
455
  ) - quotePremium
402
- : normalizeMarketOrderSlippageBuffer(params, refetchedPool.underlyingMint) +
456
+ : normalizeMarketOrderSlippageBuffer(params, paymentAccounts.paymentMint) +
403
457
  onchainPad(quotePremium);
404
458
  const maxPremiumAmount = quotePremium + slippageBuffer;
405
459
  assertPositiveAmount(maxPremiumAmount, "maxPremiumAmount");
406
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
+
407
479
  const marketDataAccount = await fetchMarketDataAccount(params.rpc, resolved.marketData);
408
480
  invariant(
409
481
  !!marketDataAccount,
@@ -419,21 +491,8 @@ export async function buildBuyFromPoolMarketOrderTransactionWithDerivation(
419
491
  ? toAddress(params.switchboardQueue)
420
492
  : getDefaultSwitchboardQueueAddress(network);
421
493
  return buildBuyFromPoolTransaction({
422
- optionPool: resolved.optionPool,
423
- optionAccount: resolved.optionAccount,
424
- longMint: resolved.longMint,
425
- underlyingMint: refetchedPool.underlyingMint,
426
- marketData: resolved.marketData,
494
+ ...marketBuyParams,
427
495
  switchboardQueue,
428
- buyer: params.buyer,
429
- buyerPaymentAccount: params.buyerPaymentAccount,
430
- escrowLongAccount: refetchedPool.escrowLongAccount,
431
- premiumVault: refetchedPool.premiumVault,
432
- quantity: params.quantity,
433
- premiumAmount: maxPremiumAmount,
434
- buyerPosition,
435
- buyerOptionAccount,
436
- remainingAccounts,
437
496
  });
438
497
  }
439
498
 
@@ -452,21 +511,8 @@ export async function buildBuyFromPoolMarketOrderTransactionWithDerivation(
452
511
  });
453
512
 
454
513
  const actionTx = await buildBuyFromPoolTransaction({
455
- optionPool: resolved.optionPool,
456
- optionAccount: resolved.optionAccount,
457
- longMint: resolved.longMint,
458
- underlyingMint: refetchedPool.underlyingMint,
459
- marketData: resolved.marketData,
514
+ ...marketBuyParams,
460
515
  switchboardQueue: getDefaultSwitchboardQueueAddress(network),
461
- buyer: params.buyer,
462
- buyerPaymentAccount: params.buyerPaymentAccount,
463
- escrowLongAccount: refetchedPool.escrowLongAccount,
464
- premiumVault: refetchedPool.premiumVault,
465
- quantity: params.quantity,
466
- premiumAmount: maxPremiumAmount,
467
- buyerPosition,
468
- buyerOptionAccount,
469
- remainingAccounts,
470
516
  });
471
517
 
472
518
  return prependSwitchboardQuote(quote, actionTx);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@epicentral/sos-sdk",
3
- "version": "0.12.0-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 {
@@ -72,3 +73,42 @@ export function calculateRequiredCollateralLegacy(
72
73
  "physical"
73
74
  );
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
+ );
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
 
@@ -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)