@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/README.md +28 -13
- package/generated/instructions/buyFromPool.ts +72 -8
- package/generated/instructions/closeLongToPool.ts +75 -18
- package/generated/instructions/optionMint.ts +4 -4
- package/generated/instructions/unwindWriterUnsold.ts +9 -9
- package/long/builders.ts +141 -69
- package/package.json +1 -1
- package/shared/amounts.ts +78 -17
- package/shared/balances.ts +42 -7
- package/short/builders.ts +10 -4
- package/short/preflight.ts +6 -5
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
750
|
+
buyerPayoutAccount,
|
|
751
|
+
buyerUnderlyingPayoutAccount,
|
|
680
752
|
collateralVault: resolved.collateralVault!,
|
|
681
753
|
quantity: params.quantity,
|
|
682
754
|
minPayoutAmount: params.minPayoutAmount,
|
package/package.json
CHANGED
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
|
|
29
|
-
* Matches on-chain
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
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
|
-
|
|
41
|
-
tokenDecimals: number
|
|
41
|
+
conversionPrice: number,
|
|
42
|
+
tokenDecimals: number,
|
|
43
|
+
settlementMode: SettlementMode = "physical"
|
|
42
44
|
): number {
|
|
43
|
-
|
|
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;
|
|
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
|
-
|
|
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
|
}
|
package/shared/balances.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
35
|
+
if (!accountInfo) return null;
|
|
20
36
|
const [b64] = accountInfo.data;
|
|
21
|
-
if (!b64) return
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
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,
|
|
738
|
+
const omlpVault = await deriveAssociatedTokenAddress(vaultPda, collateralMint);
|
|
737
739
|
const feeWallet = vault
|
|
738
|
-
? await deriveAssociatedTokenAddress(vault.feeWallet,
|
|
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,
|
|
807
|
+
deriveAssociatedTokenAddress(params.writer, collateralMint),
|
|
802
808
|
deriveWriterPositionPda(resolved.optionPool, params.writer, params.programId),
|
|
803
809
|
]);
|
|
804
810
|
|
package/short/preflight.ts
CHANGED
|
@@ -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
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
459
|
+
const premiumVaultMint = await fetchTokenAccountMint(
|
|
460
|
+
params.rpc,
|
|
461
|
+
resolved.premiumVault!
|
|
462
|
+
);
|
|
462
463
|
const premiumMintMatchesCollateralMint =
|
|
463
|
-
toAddress(
|
|
464
|
+
toAddress(premiumVaultMint) === toAddress(collateralPoolData.collateralMint);
|
|
464
465
|
|
|
465
466
|
const thetaToDebt = premiumMintMatchesCollateralMint
|
|
466
467
|
? minBigInt(thetaAvailable, proportionalTotalOwed)
|