@epicentral/sos-sdk 0.9.0-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/.env.example +1 -0
- package/AGENTS.md +7 -0
- package/LICENSE +21 -0
- package/README.md +568 -0
- package/accounts/fetchers.ts +196 -0
- package/accounts/list.ts +184 -0
- package/accounts/pdas.ts +325 -0
- package/accounts/resolve-option.ts +104 -0
- package/client/lookup-table.ts +114 -0
- package/client/program.ts +13 -0
- package/client/types.ts +9 -0
- package/generated/accounts/collateralPool.ts +217 -0
- package/generated/accounts/config.ts +156 -0
- package/generated/accounts/escrowState.ts +183 -0
- package/generated/accounts/index.ts +20 -0
- package/generated/accounts/lenderPosition.ts +211 -0
- package/generated/accounts/makerCollateralShare.ts +229 -0
- package/generated/accounts/marketDataAccount.ts +176 -0
- package/generated/accounts/optionAccount.ts +247 -0
- package/generated/accounts/optionPool.ts +285 -0
- package/generated/accounts/poolLoan.ts +232 -0
- package/generated/accounts/positionAccount.ts +201 -0
- package/generated/accounts/vault.ts +366 -0
- package/generated/accounts/writerPosition.ts +327 -0
- package/generated/errors/index.ts +9 -0
- package/generated/errors/optionProgram.ts +476 -0
- package/generated/index.ts +13 -0
- package/generated/instructions/acceptAdmin.ts +230 -0
- package/generated/instructions/autoExerciseAllExpired.ts +685 -0
- package/generated/instructions/autoExerciseExpired.ts +754 -0
- package/generated/instructions/borrowFromPool.ts +619 -0
- package/generated/instructions/buyFromPool.ts +761 -0
- package/generated/instructions/closeLongToPool.ts +762 -0
- package/generated/instructions/closeOption.ts +235 -0
- package/generated/instructions/createEscrowV2.ts +518 -0
- package/generated/instructions/depositCollateral.ts +624 -0
- package/generated/instructions/depositToPosition.ts +429 -0
- package/generated/instructions/index.ts +47 -0
- package/generated/instructions/initCollateralPool.ts +513 -0
- package/generated/instructions/initConfig.ts +279 -0
- package/generated/instructions/initOptionPool.ts +587 -0
- package/generated/instructions/initializeMarketData.ts +359 -0
- package/generated/instructions/liquidateWriterPosition.ts +750 -0
- package/generated/instructions/liquidateWriterPositionRescue.ts +623 -0
- package/generated/instructions/omlpCreateVault.ts +553 -0
- package/generated/instructions/omlpUpdateFeeWallet.ts +473 -0
- package/generated/instructions/omlpUpdateInterestModel.ts +322 -0
- package/generated/instructions/omlpUpdateLiquidationThreshold.ts +304 -0
- package/generated/instructions/omlpUpdateMaintenanceBuffer.ts +304 -0
- package/generated/instructions/omlpUpdateMaxBorrowCap.ts +304 -0
- package/generated/instructions/omlpUpdateMaxLeverage.ts +304 -0
- package/generated/instructions/omlpUpdateProtocolFee.ts +304 -0
- package/generated/instructions/omlpUpdateSupplyLimit.ts +304 -0
- package/generated/instructions/optionExercise.ts +617 -0
- package/generated/instructions/optionMint.ts +1373 -0
- package/generated/instructions/optionValidate.ts +302 -0
- package/generated/instructions/repayPoolLoan.ts +558 -0
- package/generated/instructions/repayPoolLoanFromCollateral.ts +514 -0
- package/generated/instructions/repayPoolLoanFromWallet.ts +542 -0
- package/generated/instructions/settleMakerCollateral.ts +509 -0
- package/generated/instructions/syncWriterPosition.ts +206 -0
- package/generated/instructions/transferAdmin.ts +245 -0
- package/generated/instructions/unwindWriterUnsold.ts +764 -0
- package/generated/instructions/updateImpliedVolatility.ts +226 -0
- package/generated/instructions/updateMarketData.ts +315 -0
- package/generated/instructions/withdrawFromPosition.ts +405 -0
- package/generated/instructions/writeToPool.ts +619 -0
- package/generated/programs/index.ts +9 -0
- package/generated/programs/optionProgram.ts +1144 -0
- package/generated/shared/index.ts +164 -0
- package/generated/types/impliedVolatilityUpdated.ts +73 -0
- package/generated/types/index.ts +28 -0
- package/generated/types/liquidationExecuted.ts +73 -0
- package/generated/types/liquidationRescueEvent.ts +82 -0
- package/generated/types/marketDataInitialized.ts +61 -0
- package/generated/types/marketDataUpdated.ts +69 -0
- package/generated/types/optionClosed.ts +56 -0
- package/generated/types/optionExercised.ts +62 -0
- package/generated/types/optionExpired.ts +49 -0
- package/generated/types/optionMinted.ts +78 -0
- package/generated/types/optionType.ts +38 -0
- package/generated/types/optionValidated.ts +82 -0
- package/generated/types/poolLoanCreated.ts +74 -0
- package/generated/types/poolLoanRepaid.ts +74 -0
- package/generated/types/positionDeposited.ts +73 -0
- package/generated/types/positionWithdrawn.ts +81 -0
- package/generated/types/protocolFeeUpdated.ts +69 -0
- package/generated/types/vaultCreated.ts +60 -0
- package/generated/types/vaultFeeWalletUpdated.ts +67 -0
- package/generated/types/vaultInterestModelUpdated.ts +77 -0
- package/generated/types/vaultLiquidationThresholdUpdated.ts +69 -0
- package/index.ts +68 -0
- package/long/builders.ts +690 -0
- package/long/exercise.ts +123 -0
- package/long/preflight.ts +214 -0
- package/long/quotes.ts +48 -0
- package/long/remaining-accounts.ts +111 -0
- package/omlp/builders.ts +94 -0
- package/omlp/service.ts +136 -0
- package/oracle/switchboard.ts +315 -0
- package/package.json +34 -0
- package/shared/amounts.ts +53 -0
- package/shared/balances.ts +57 -0
- package/shared/errors.ts +12 -0
- package/shared/remaining-accounts.ts +41 -0
- package/shared/trade-config.ts +27 -0
- package/shared/transactions.ts +121 -0
- package/short/builders.ts +874 -0
- package/short/close-option.ts +34 -0
- package/short/pool.ts +189 -0
- package/short/preflight.ts +619 -0
- package/tsconfig.json +13 -0
- package/wsol/instructions.ts +247 -0
package/long/exercise.ts
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { getOptionExerciseInstruction } from "../generated/instructions";
|
|
2
|
+
import type { Instruction } from "@solana/kit";
|
|
3
|
+
import { toAddress } from "../client/program";
|
|
4
|
+
import type { AddressLike, BuiltTransaction, KitRpc } from "../client/types";
|
|
5
|
+
import { fetchMarketDataAccount } from "../accounts/fetchers";
|
|
6
|
+
import { invariant } from "../shared/errors";
|
|
7
|
+
import {
|
|
8
|
+
buildSwitchboardQuoteInstruction,
|
|
9
|
+
feedIdBytesToHex,
|
|
10
|
+
getDefaultSwitchboardQueueAddress,
|
|
11
|
+
inferSwitchboardNetwork,
|
|
12
|
+
prependSwitchboardQuote,
|
|
13
|
+
} from "../oracle/switchboard";
|
|
14
|
+
|
|
15
|
+
export interface BuildOptionExerciseParams {
|
|
16
|
+
optionAccount: AddressLike;
|
|
17
|
+
positionAccount: AddressLike;
|
|
18
|
+
marketData: AddressLike;
|
|
19
|
+
underlyingMint: AddressLike;
|
|
20
|
+
/** Switchboard queue account (defaults when `disableSwitchboardCrank` + `rpc`). */
|
|
21
|
+
switchboardQueue?: AddressLike;
|
|
22
|
+
buyerPaymentAccount: AddressLike;
|
|
23
|
+
makerCollateralAccount: AddressLike;
|
|
24
|
+
escrowState: AddressLike;
|
|
25
|
+
escrowTokenAccount: AddressLike;
|
|
26
|
+
escrowAuthority: AddressLike;
|
|
27
|
+
buyer: AddressLike;
|
|
28
|
+
tokenProgram?: AddressLike;
|
|
29
|
+
rpc?: KitRpc;
|
|
30
|
+
/** HTTP RPC URL — required to fetch Switchboard quote ix unless `disableSwitchboardCrank`. */
|
|
31
|
+
rpcEndpoint?: string;
|
|
32
|
+
disableSwitchboardCrank?: boolean;
|
|
33
|
+
switchboardCrossbarUrl?: string;
|
|
34
|
+
switchboardNumSignatures?: number;
|
|
35
|
+
/**
|
|
36
|
+
* 0-based index of the quote ix after wallet prepends. OPX `sendInstructions` (limit + price) → **2** by default.
|
|
37
|
+
*/
|
|
38
|
+
switchboardQuoteInstructionIndex?: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Legacy escrow-based option exercise. Prefer pool flows: close_long_to_pool and auto_exercise_expired.
|
|
43
|
+
* @deprecated Use buildCloseLongToPoolTransaction for closing longs and rely on auto_exercise_expired for expiration.
|
|
44
|
+
*/
|
|
45
|
+
export function buildOptionExerciseInstruction(
|
|
46
|
+
params: BuildOptionExerciseParams
|
|
47
|
+
): Instruction<string> {
|
|
48
|
+
invariant(
|
|
49
|
+
!!params.switchboardQueue,
|
|
50
|
+
"switchboardQueue is required to build option exercise instruction."
|
|
51
|
+
);
|
|
52
|
+
return getOptionExerciseInstruction({
|
|
53
|
+
optionAccount: toAddress(params.optionAccount),
|
|
54
|
+
positionAccount: toAddress(params.positionAccount),
|
|
55
|
+
marketData: toAddress(params.marketData),
|
|
56
|
+
underlyingMint: toAddress(params.underlyingMint),
|
|
57
|
+
switchboardQueue: toAddress(params.switchboardQueue),
|
|
58
|
+
buyerPaymentAccount: toAddress(params.buyerPaymentAccount),
|
|
59
|
+
makerCollateralAccount: toAddress(params.makerCollateralAccount),
|
|
60
|
+
escrowState: toAddress(params.escrowState),
|
|
61
|
+
escrowTokenAccount: toAddress(params.escrowTokenAccount),
|
|
62
|
+
escrowAuthority: toAddress(params.escrowAuthority),
|
|
63
|
+
buyer: toAddress(params.buyer) as any,
|
|
64
|
+
tokenProgram: params.tokenProgram ? toAddress(params.tokenProgram) : undefined,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Builds an option exercise transaction (escrow/ask-based flow).
|
|
70
|
+
* Prefer pool-based flows: buildCloseLongToPoolTransaction for closing longs and auto_exercise_expired for expired ITM.
|
|
71
|
+
* @deprecated Use buildCloseLongToPoolTransaction and auto_exercise_expired; do not use for new flows.
|
|
72
|
+
*/
|
|
73
|
+
export async function buildOptionExerciseTransaction(
|
|
74
|
+
params: BuildOptionExerciseParams
|
|
75
|
+
): Promise<BuiltTransaction> {
|
|
76
|
+
if (params.disableSwitchboardCrank === true) {
|
|
77
|
+
const switchboardQueue = params.switchboardQueue
|
|
78
|
+
? toAddress(params.switchboardQueue)
|
|
79
|
+
: params.rpc
|
|
80
|
+
? getDefaultSwitchboardQueueAddress(await inferSwitchboardNetwork(params.rpc))
|
|
81
|
+
: undefined;
|
|
82
|
+
invariant(
|
|
83
|
+
!!switchboardQueue,
|
|
84
|
+
"switchboardQueue or rpc is required when disableSwitchboardCrank is set."
|
|
85
|
+
);
|
|
86
|
+
const instruction = buildOptionExerciseInstruction({
|
|
87
|
+
...params,
|
|
88
|
+
switchboardQueue,
|
|
89
|
+
});
|
|
90
|
+
return { instructions: [instruction] };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
invariant(
|
|
94
|
+
!!params.rpc && !!params.rpcEndpoint,
|
|
95
|
+
"rpc and rpcEndpoint are required to prepend Switchboard quote (or set disableSwitchboardCrank)."
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const marketDataAccount = await fetchMarketDataAccount(
|
|
99
|
+
params.rpc,
|
|
100
|
+
params.marketData
|
|
101
|
+
);
|
|
102
|
+
invariant(!!marketDataAccount, "Market data account not found.");
|
|
103
|
+
const feedIdHex = feedIdBytesToHex(
|
|
104
|
+
Uint8Array.from(marketDataAccount.switchboardFeedId as unknown as Uint8Array)
|
|
105
|
+
);
|
|
106
|
+
const network = await inferSwitchboardNetwork(params.rpc);
|
|
107
|
+
|
|
108
|
+
const quote = await buildSwitchboardQuoteInstruction({
|
|
109
|
+
rpcEndpoint: params.rpcEndpoint,
|
|
110
|
+
feedIdHex,
|
|
111
|
+
network,
|
|
112
|
+
crossbarUrl: params.switchboardCrossbarUrl,
|
|
113
|
+
numSignatures: params.switchboardNumSignatures,
|
|
114
|
+
instructionIdx: params.switchboardQuoteInstructionIndex ?? 2,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const instruction = buildOptionExerciseInstruction({
|
|
118
|
+
...params,
|
|
119
|
+
switchboardQueue: getDefaultSwitchboardQueueAddress(network),
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
return prependSwitchboardQuote(quote, { instructions: [instruction] });
|
|
123
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import type { AddressLike, KitRpc } from "../client/types";
|
|
2
|
+
import type { OptionType } from "../generated/types";
|
|
3
|
+
import { fetchWriterPositionsForPool } from "../accounts/list";
|
|
4
|
+
import { resolveOptionAccounts } from "../accounts/resolve-option";
|
|
5
|
+
import { fetchOptionPool } from "../accounts/fetchers";
|
|
6
|
+
import { assertNonNegativeAmount, assertPositiveAmount } from "../shared/amounts";
|
|
7
|
+
import { invariant } from "../shared/errors";
|
|
8
|
+
|
|
9
|
+
function toBigInt(value: bigint | number): bigint {
|
|
10
|
+
return typeof value === "bigint" ? value : BigInt(value);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface PreflightBuyFromPoolMarketOrderParams {
|
|
14
|
+
underlyingAsset: AddressLike;
|
|
15
|
+
optionType: OptionType;
|
|
16
|
+
strikePrice: number;
|
|
17
|
+
expirationDate: bigint | number;
|
|
18
|
+
quantity: bigint | number;
|
|
19
|
+
rpc: KitRpc;
|
|
20
|
+
programId?: AddressLike;
|
|
21
|
+
quotedPremiumTotal?: bigint | number;
|
|
22
|
+
slippageBufferBaseUnits?: bigint | number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface BuyFromPoolMarketOrderPremiumSummary {
|
|
26
|
+
quotedPremiumTotal: bigint;
|
|
27
|
+
slippageBufferBaseUnits: bigint;
|
|
28
|
+
maxPremiumAmount: bigint;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface PreflightBuyFromPoolMarketOrderResult {
|
|
32
|
+
canBuy: boolean;
|
|
33
|
+
reason?: string;
|
|
34
|
+
poolTotalAvailable: bigint;
|
|
35
|
+
requestedQuantity: bigint;
|
|
36
|
+
remainingAccountsCount: number;
|
|
37
|
+
remainingUnsoldAggregate: bigint;
|
|
38
|
+
premium?: BuyFromPoolMarketOrderPremiumSummary;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function preflightBuyFromPoolMarketOrder(
|
|
42
|
+
params: PreflightBuyFromPoolMarketOrderParams
|
|
43
|
+
): Promise<PreflightBuyFromPoolMarketOrderResult> {
|
|
44
|
+
assertPositiveAmount(params.quantity, "quantity");
|
|
45
|
+
const requestedQuantity = toBigInt(params.quantity);
|
|
46
|
+
|
|
47
|
+
const resolved = await resolveOptionAccounts({
|
|
48
|
+
underlyingAsset: params.underlyingAsset,
|
|
49
|
+
optionType: params.optionType,
|
|
50
|
+
strikePrice: params.strikePrice,
|
|
51
|
+
expirationDate: params.expirationDate,
|
|
52
|
+
programId: params.programId,
|
|
53
|
+
rpc: params.rpc,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const [optionPool, writerPositions] = await Promise.all([
|
|
57
|
+
fetchOptionPool(params.rpc, resolved.optionPool),
|
|
58
|
+
fetchWriterPositionsForPool(params.rpc, resolved.optionPool, params.programId),
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
invariant(
|
|
62
|
+
!!optionPool,
|
|
63
|
+
"Option pool must exist; ensure rpc is provided and pool is initialized."
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
// Filter out inactive positions (settled, liquidated, or zero unsold)
|
|
67
|
+
const activeWriterPositions = writerPositions.filter(
|
|
68
|
+
({ data }) => !data.isSettled && !data.isLiquidated && toBigInt(data.unsoldQty) > 0n
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
// Use active positions for coverage calculation
|
|
72
|
+
const availableWriterPositions = activeWriterPositions;
|
|
73
|
+
const remainingUnsoldAggregate = availableWriterPositions.reduce(
|
|
74
|
+
(acc, { data }) => acc + toBigInt(data.unsoldQty),
|
|
75
|
+
0n
|
|
76
|
+
);
|
|
77
|
+
const poolTotalAvailable = toBigInt(optionPool.totalAvailable);
|
|
78
|
+
|
|
79
|
+
const hasPoolLiquidity = poolTotalAvailable >= requestedQuantity;
|
|
80
|
+
const hasWriterCoverage = remainingUnsoldAggregate >= requestedQuantity;
|
|
81
|
+
|
|
82
|
+
let reason: string | undefined;
|
|
83
|
+
if (!hasPoolLiquidity) {
|
|
84
|
+
reason = "Pool total_available is less than requested quantity.";
|
|
85
|
+
} else if (!hasWriterCoverage) {
|
|
86
|
+
reason =
|
|
87
|
+
"Remaining writer-position liquidity is insufficient to fully fill requested quantity.";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const result: PreflightBuyFromPoolMarketOrderResult = {
|
|
91
|
+
canBuy: hasPoolLiquidity && hasWriterCoverage,
|
|
92
|
+
reason,
|
|
93
|
+
poolTotalAvailable,
|
|
94
|
+
requestedQuantity,
|
|
95
|
+
remainingAccountsCount: availableWriterPositions.length,
|
|
96
|
+
remainingUnsoldAggregate,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
if (params.quotedPremiumTotal !== undefined) {
|
|
100
|
+
assertPositiveAmount(params.quotedPremiumTotal, "quotedPremiumTotal");
|
|
101
|
+
if (params.slippageBufferBaseUnits !== undefined) {
|
|
102
|
+
assertNonNegativeAmount(params.slippageBufferBaseUnits, "slippageBufferBaseUnits");
|
|
103
|
+
}
|
|
104
|
+
const quotedPremiumTotal = toBigInt(params.quotedPremiumTotal);
|
|
105
|
+
const slippageBufferBaseUnits =
|
|
106
|
+
params.slippageBufferBaseUnits !== undefined
|
|
107
|
+
? toBigInt(params.slippageBufferBaseUnits)
|
|
108
|
+
: 0n;
|
|
109
|
+
|
|
110
|
+
result.premium = {
|
|
111
|
+
quotedPremiumTotal,
|
|
112
|
+
slippageBufferBaseUnits,
|
|
113
|
+
maxPremiumAmount: quotedPremiumTotal + slippageBufferBaseUnits,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface PreflightCloseLongToPoolParams {
|
|
121
|
+
underlyingAsset: AddressLike;
|
|
122
|
+
optionType: OptionType;
|
|
123
|
+
strikePrice: number;
|
|
124
|
+
expirationDate: bigint | number;
|
|
125
|
+
quantity: bigint | number;
|
|
126
|
+
rpc: KitRpc;
|
|
127
|
+
programId?: AddressLike;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export interface PreflightCloseLongToPoolResult {
|
|
131
|
+
/**
|
|
132
|
+
* Safe to submit the close-long transaction. When `false`, inspect `reason`.
|
|
133
|
+
* Note: the buyer's own balance (`buyer_position.quantity >= requested`) is
|
|
134
|
+
* enforced on-chain and not rechecked here.
|
|
135
|
+
*/
|
|
136
|
+
canClose: boolean;
|
|
137
|
+
reason?: string;
|
|
138
|
+
requestedQuantity: bigint;
|
|
139
|
+
/** `OptionPool.total_sold_qty` at preflight time. */
|
|
140
|
+
poolTotalSoldQty: bigint;
|
|
141
|
+
/**
|
|
142
|
+
* Sum of `sold_qty` across active WriterPositions returned by
|
|
143
|
+
* `getCloseLongToPoolRemainingAccounts`. Must equal `poolTotalSoldQty` for the
|
|
144
|
+
* on-chain Hamilton completeness check to pass.
|
|
145
|
+
*/
|
|
146
|
+
activeWriterSoldSum: bigint;
|
|
147
|
+
/** Number of WriterPosition accounts the SDK will pass as remaining_accounts. */
|
|
148
|
+
remainingAccountsCount: number;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export async function preflightCloseLongToPool(
|
|
152
|
+
params: PreflightCloseLongToPoolParams
|
|
153
|
+
): Promise<PreflightCloseLongToPoolResult> {
|
|
154
|
+
assertPositiveAmount(params.quantity, "quantity");
|
|
155
|
+
const requestedQuantity = toBigInt(params.quantity);
|
|
156
|
+
|
|
157
|
+
const resolved = await resolveOptionAccounts({
|
|
158
|
+
underlyingAsset: params.underlyingAsset,
|
|
159
|
+
optionType: params.optionType,
|
|
160
|
+
strikePrice: params.strikePrice,
|
|
161
|
+
expirationDate: params.expirationDate,
|
|
162
|
+
programId: params.programId,
|
|
163
|
+
rpc: params.rpc,
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const [optionPool, writerPositions] = await Promise.all([
|
|
167
|
+
fetchOptionPool(params.rpc, resolved.optionPool),
|
|
168
|
+
fetchWriterPositionsForPool(params.rpc, resolved.optionPool, params.programId),
|
|
169
|
+
]);
|
|
170
|
+
|
|
171
|
+
invariant(
|
|
172
|
+
!!optionPool,
|
|
173
|
+
"Option pool must exist; ensure rpc is provided and pool is initialized."
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
const active = writerPositions.filter(
|
|
177
|
+
({ data }) => !data.isSettled && !data.isLiquidated
|
|
178
|
+
);
|
|
179
|
+
const activeWriterSoldSum = active.reduce(
|
|
180
|
+
(acc, { data }) => acc + toBigInt(data.soldQty),
|
|
181
|
+
0n
|
|
182
|
+
);
|
|
183
|
+
const poolTotalSoldQty = toBigInt(optionPool.totalSoldQty);
|
|
184
|
+
|
|
185
|
+
const hasInventory = poolTotalSoldQty > 0n;
|
|
186
|
+
const hasWriters = active.length > 0;
|
|
187
|
+
const sumMatches = activeWriterSoldSum === poolTotalSoldQty;
|
|
188
|
+
const hasCapacity = requestedQuantity <= poolTotalSoldQty;
|
|
189
|
+
|
|
190
|
+
let reason: string | undefined;
|
|
191
|
+
if (!hasInventory) {
|
|
192
|
+
reason =
|
|
193
|
+
"Pool has no sold inventory to close against (total_sold_qty is zero).";
|
|
194
|
+
} else if (!hasWriters) {
|
|
195
|
+
reason =
|
|
196
|
+
"Pool reports sold inventory but no active WriterPositions were returned by RPC (data staleness?).";
|
|
197
|
+
} else if (!sumMatches) {
|
|
198
|
+
reason =
|
|
199
|
+
`Active WriterPosition sold_qty sum (${activeWriterSoldSum}) does not match OptionPool.total_sold_qty (${poolTotalSoldQty}). ` +
|
|
200
|
+
`Refetch and retry; a new mint or close may have just landed.`;
|
|
201
|
+
} else if (!hasCapacity) {
|
|
202
|
+
reason =
|
|
203
|
+
`Requested close quantity (${requestedQuantity}) exceeds pool sold inventory (${poolTotalSoldQty}).`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
canClose: hasInventory && hasWriters && sumMatches && hasCapacity,
|
|
208
|
+
reason,
|
|
209
|
+
requestedQuantity,
|
|
210
|
+
poolTotalSoldQty,
|
|
211
|
+
activeWriterSoldSum,
|
|
212
|
+
remainingAccountsCount: active.length,
|
|
213
|
+
};
|
|
214
|
+
}
|
package/long/quotes.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { assertPositiveAmount } from "../shared/amounts";
|
|
2
|
+
import { SdkValidationError } from "../shared/errors";
|
|
3
|
+
|
|
4
|
+
export function applySlippageBps(amount: bigint | number, slippageBps: number): bigint {
|
|
5
|
+
if (slippageBps < 0) {
|
|
6
|
+
throw new SdkValidationError("slippageBps cannot be negative.");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const base = BigInt(amount);
|
|
10
|
+
return (base * BigInt(10_000 + slippageBps)) / 10_000n;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function applyMinSlippageBps(amount: bigint | number, slippageBps: number): bigint {
|
|
14
|
+
if (slippageBps < 0) {
|
|
15
|
+
throw new SdkValidationError("slippageBps cannot be negative.");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const base = BigInt(amount);
|
|
19
|
+
return (base * BigInt(10_000 - slippageBps)) / 10_000n;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function buildBuyQuote(params: {
|
|
23
|
+
quantity: bigint | number;
|
|
24
|
+
premiumPerContract: bigint | number;
|
|
25
|
+
maxSlippageBps: number;
|
|
26
|
+
}): { expectedPremium: bigint; maxPremium: bigint } {
|
|
27
|
+
assertPositiveAmount(params.quantity, "quantity");
|
|
28
|
+
assertPositiveAmount(params.premiumPerContract, "premiumPerContract");
|
|
29
|
+
|
|
30
|
+
const expectedPremium = BigInt(params.quantity) * BigInt(params.premiumPerContract);
|
|
31
|
+
return {
|
|
32
|
+
expectedPremium,
|
|
33
|
+
maxPremium: applySlippageBps(expectedPremium, params.maxSlippageBps),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function buildCloseQuote(params: {
|
|
38
|
+
expectedPayout: bigint | number;
|
|
39
|
+
maxSlippageBps: number;
|
|
40
|
+
}): { expectedPayout: bigint; minPayout: bigint } {
|
|
41
|
+
assertPositiveAmount(params.expectedPayout, "expectedPayout");
|
|
42
|
+
|
|
43
|
+
const expectedPayout = BigInt(params.expectedPayout);
|
|
44
|
+
return {
|
|
45
|
+
expectedPayout,
|
|
46
|
+
minPayout: applyMinSlippageBps(expectedPayout, params.maxSlippageBps),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import bs58 from "bs58";
|
|
2
|
+
import type { RemainingAccountInput } from "../shared/remaining-accounts";
|
|
3
|
+
import type { AddressLike, KitRpc } from "../client/types";
|
|
4
|
+
import { PROGRAM_ID } from "../client/program";
|
|
5
|
+
import { fetchWriterPositionsForPool } from "../accounts/list";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Byte-wise comparison of two base58-encoded pubkeys. Matches the on-chain
|
|
9
|
+
* `Pubkey::cmp` ordering over the 32-byte buffer — base58 string compare is
|
|
10
|
+
* NOT equivalent because the encoded strings can have different lengths.
|
|
11
|
+
*/
|
|
12
|
+
function comparePubkeyBytes(a: string, b: string): number {
|
|
13
|
+
const ab = bs58.decode(a);
|
|
14
|
+
const bb = bs58.decode(b);
|
|
15
|
+
const len = Math.min(ab.length, bb.length);
|
|
16
|
+
for (let i = 0; i < len; i++) {
|
|
17
|
+
if (ab[i]! !== bb[i]!) return ab[i]! - bb[i]!;
|
|
18
|
+
}
|
|
19
|
+
return ab.length - bb.length;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* FIFO ordering used by both `buy_from_pool` verification and the
|
|
24
|
+
* `close_long_to_pool` Hamilton redistribution: primary key `createdAt`
|
|
25
|
+
* ascending, tiebreaker pubkey bytes ascending. MUST stay in sync with the
|
|
26
|
+
* on-chain comparator in `option-program/programs/option-program/src/pool.rs`.
|
|
27
|
+
*/
|
|
28
|
+
function compareByCreatedAtThenPubkey(
|
|
29
|
+
a: { createdAt: bigint; address: string },
|
|
30
|
+
b: { createdAt: bigint; address: string }
|
|
31
|
+
): number {
|
|
32
|
+
if (a.createdAt < b.createdAt) return -1;
|
|
33
|
+
if (a.createdAt > b.createdAt) return 1;
|
|
34
|
+
return comparePubkeyBytes(a.address, b.address);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Returns remaining_accounts for the `buy_from_pool` instruction: active
|
|
39
|
+
* WriterPositions for the given pool, sorted by (`createdAt` asc, `pubkey` asc).
|
|
40
|
+
*
|
|
41
|
+
* The on-chain program verifies this ordering and rejects unsorted lists with
|
|
42
|
+
* `BuyRemainingAccountsUnsorted`. Positions with no unsold inventory cannot fill
|
|
43
|
+
* any buy and are excluded.
|
|
44
|
+
*/
|
|
45
|
+
export async function getBuyFromPoolRemainingAccounts(
|
|
46
|
+
rpc: KitRpc,
|
|
47
|
+
optionPool: AddressLike,
|
|
48
|
+
programId?: AddressLike
|
|
49
|
+
): Promise<RemainingAccountInput[]> {
|
|
50
|
+
const positions = await fetchWriterPositionsForPool(
|
|
51
|
+
rpc,
|
|
52
|
+
optionPool,
|
|
53
|
+
programId ?? PROGRAM_ID
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const active = positions.filter(
|
|
57
|
+
(p) =>
|
|
58
|
+
!p.data.isSettled &&
|
|
59
|
+
!p.data.isLiquidated &&
|
|
60
|
+
BigInt(p.data.unsoldQty) > 0n
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const sorted = [...active].sort((a, b) =>
|
|
64
|
+
compareByCreatedAtThenPubkey(
|
|
65
|
+
{ createdAt: BigInt(a.data.createdAt), address: String(a.address) },
|
|
66
|
+
{ createdAt: BigInt(b.data.createdAt), address: String(b.address) }
|
|
67
|
+
)
|
|
68
|
+
);
|
|
69
|
+
return sorted.map(({ address }) => ({
|
|
70
|
+
address,
|
|
71
|
+
isWritable: true,
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Returns remaining_accounts for the `close_long_to_pool` instruction: **every
|
|
77
|
+
* active WriterPosition** for the given pool (sum of `sold_qty` must equal
|
|
78
|
+
* `OptionPool.total_sold_qty`). The on-chain Hamilton redistribution enforces a
|
|
79
|
+
* strict completeness check via `CloseLongWritersIncomplete` and fails if the
|
|
80
|
+
* caller skips any active writer.
|
|
81
|
+
*
|
|
82
|
+
* Ordering does not affect correctness on-chain (the Hamilton allocator sorts
|
|
83
|
+
* remainders itself), but we preserve the same FIFO order used by `buy_from_pool`
|
|
84
|
+
* so logs and simulations are deterministic.
|
|
85
|
+
*/
|
|
86
|
+
export async function getCloseLongToPoolRemainingAccounts(
|
|
87
|
+
rpc: KitRpc,
|
|
88
|
+
optionPool: AddressLike,
|
|
89
|
+
programId?: AddressLike
|
|
90
|
+
): Promise<RemainingAccountInput[]> {
|
|
91
|
+
const positions = await fetchWriterPositionsForPool(
|
|
92
|
+
rpc,
|
|
93
|
+
optionPool,
|
|
94
|
+
programId ?? PROGRAM_ID
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const active = positions.filter(
|
|
98
|
+
(p) => !p.data.isSettled && !p.data.isLiquidated
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const sorted = [...active].sort((a, b) =>
|
|
102
|
+
compareByCreatedAtThenPubkey(
|
|
103
|
+
{ createdAt: BigInt(a.data.createdAt), address: String(a.address) },
|
|
104
|
+
{ createdAt: BigInt(b.data.createdAt), address: String(b.address) }
|
|
105
|
+
)
|
|
106
|
+
);
|
|
107
|
+
return sorted.map(({ address }) => ({
|
|
108
|
+
address,
|
|
109
|
+
isWritable: true,
|
|
110
|
+
}));
|
|
111
|
+
}
|
package/omlp/builders.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getDepositToPositionInstructionAsync,
|
|
3
|
+
getWithdrawFromPositionInstructionAsync,
|
|
4
|
+
} from "../generated/instructions";
|
|
5
|
+
import type { Instruction } from "@solana/kit";
|
|
6
|
+
import { toAddress } from "../client/program";
|
|
7
|
+
import type { AddressLike, BuiltTransaction } from "../client/types";
|
|
8
|
+
import { assertPositiveAmount } from "../shared/amounts";
|
|
9
|
+
import { getCloseAccountInstruction, NATIVE_MINT } from "../wsol/instructions";
|
|
10
|
+
|
|
11
|
+
export interface BuildDepositToPositionParams {
|
|
12
|
+
vault: AddressLike;
|
|
13
|
+
lenderTokenAccount: AddressLike;
|
|
14
|
+
vaultTokenAccount: AddressLike;
|
|
15
|
+
lender: AddressLike;
|
|
16
|
+
amount: bigint | number;
|
|
17
|
+
position?: AddressLike;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface BuildWithdrawFromPositionParams {
|
|
21
|
+
vault: AddressLike;
|
|
22
|
+
vaultTokenAccount: AddressLike;
|
|
23
|
+
lenderTokenAccount: AddressLike;
|
|
24
|
+
lender: AddressLike;
|
|
25
|
+
amount: bigint | number;
|
|
26
|
+
position?: AddressLike;
|
|
27
|
+
unwrapSol?: boolean;
|
|
28
|
+
vaultMint?: AddressLike;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function buildDepositToPositionInstruction(
|
|
32
|
+
params: BuildDepositToPositionParams
|
|
33
|
+
): Promise<Instruction<string>> {
|
|
34
|
+
assertPositiveAmount(params.amount, "amount");
|
|
35
|
+
|
|
36
|
+
const kitInstruction = await getDepositToPositionInstructionAsync({
|
|
37
|
+
position: params.position ? toAddress(params.position) : undefined,
|
|
38
|
+
vault: toAddress(params.vault),
|
|
39
|
+
lenderTokenAccount: toAddress(params.lenderTokenAccount),
|
|
40
|
+
vaultTokenAccount: toAddress(params.vaultTokenAccount),
|
|
41
|
+
lender: toAddress(params.lender) as any,
|
|
42
|
+
amount: params.amount,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
return kitInstruction;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function buildDepositToPositionTransaction(
|
|
49
|
+
params: BuildDepositToPositionParams
|
|
50
|
+
): Promise<BuiltTransaction> {
|
|
51
|
+
const instruction = await buildDepositToPositionInstruction(params);
|
|
52
|
+
return { instructions: [instruction] };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function buildWithdrawFromPositionInstruction(
|
|
56
|
+
params: BuildWithdrawFromPositionParams
|
|
57
|
+
): Promise<Instruction<string>> {
|
|
58
|
+
assertPositiveAmount(params.amount, "amount");
|
|
59
|
+
|
|
60
|
+
const kitInstruction = await getWithdrawFromPositionInstructionAsync({
|
|
61
|
+
position: params.position ? toAddress(params.position) : undefined,
|
|
62
|
+
vault: toAddress(params.vault),
|
|
63
|
+
vaultTokenAccount: toAddress(params.vaultTokenAccount),
|
|
64
|
+
lenderTokenAccount: toAddress(params.lenderTokenAccount),
|
|
65
|
+
lender: toAddress(params.lender) as any,
|
|
66
|
+
amount: params.amount,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
return kitInstruction;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function buildWithdrawFromPositionTransaction(
|
|
73
|
+
params: BuildWithdrawFromPositionParams
|
|
74
|
+
): Promise<BuiltTransaction> {
|
|
75
|
+
const withdrawInstruction = await buildWithdrawFromPositionInstruction(params);
|
|
76
|
+
const instructions: Instruction<string>[] = [withdrawInstruction];
|
|
77
|
+
|
|
78
|
+
const shouldUnwrapSol =
|
|
79
|
+
params.unwrapSol === true &&
|
|
80
|
+
params.vaultMint !== undefined &&
|
|
81
|
+
toAddress(params.vaultMint) === toAddress(NATIVE_MINT);
|
|
82
|
+
|
|
83
|
+
if (shouldUnwrapSol) {
|
|
84
|
+
instructions.push(
|
|
85
|
+
getCloseAccountInstruction(
|
|
86
|
+
params.lenderTokenAccount,
|
|
87
|
+
params.lender,
|
|
88
|
+
params.lender
|
|
89
|
+
)
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return { instructions };
|
|
94
|
+
}
|