@epicentral/sos-sdk 0.4.0-alpha.1 → 0.4.0-alpha.2
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 +41 -5
- package/client/lookup-table.ts +1 -1
- package/index.ts +1 -0
- package/long/builders.ts +116 -2
- package/long/preflight.ts +114 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -47,7 +47,9 @@ Additional modules:
|
|
|
47
47
|
|
|
48
48
|
| Function | Description |
|
|
49
49
|
|----------|-------------|
|
|
50
|
+
| `buildBuyFromPoolMarketOrderTransactionWithDerivation` | High-level market-order buy builder (refetches pool + remaining accounts, applies premium cap buffer). |
|
|
50
51
|
| `buildBuyFromPoolTransactionWithDerivation` | Builds buy-from-pool transaction; resolves accounts from option identity. |
|
|
52
|
+
| `preflightBuyFromPoolMarketOrder` | Buy preflight helper for liquidity + remaining-account coverage checks. |
|
|
51
53
|
| `buildCloseLongToPoolTransactionWithDerivation` | Builds close-long-to-pool transaction. |
|
|
52
54
|
| `getBuyFromPoolRemainingAccounts` | Builds remaining_accounts for buy (writer positions, etc.). |
|
|
53
55
|
|
|
@@ -148,16 +150,31 @@ const tx = await buildUnwindWriterUnsoldWithLoanRepayment({
|
|
|
148
150
|
|
|
149
151
|
## Usage Examples
|
|
150
152
|
|
|
151
|
-
### Buy
|
|
153
|
+
### Buy From Pool (market order, high-level)
|
|
152
154
|
|
|
153
155
|
```ts
|
|
154
156
|
import {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
+
buildBuyFromPoolMarketOrderTransactionWithDerivation,
|
|
158
|
+
preflightBuyFromPoolMarketOrder,
|
|
157
159
|
OptionType,
|
|
158
160
|
} from "@epicentral/sos-sdk";
|
|
159
161
|
|
|
160
|
-
const
|
|
162
|
+
const preflight = await preflightBuyFromPoolMarketOrder({
|
|
163
|
+
underlyingAsset: "...",
|
|
164
|
+
optionType: OptionType.Call,
|
|
165
|
+
strikePrice: 100_000,
|
|
166
|
+
expirationDate: BigInt(1735689600),
|
|
167
|
+
quantity: 1_000_000,
|
|
168
|
+
rpc,
|
|
169
|
+
quotedPremiumTotal: 50_000,
|
|
170
|
+
slippageBufferBaseUnits: 500_000n,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
if (!preflight.canBuy) {
|
|
174
|
+
throw new Error(preflight.reason ?? "Buy preflight failed");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const tx = await buildBuyFromPoolMarketOrderTransactionWithDerivation({
|
|
161
178
|
underlyingAsset: "...",
|
|
162
179
|
optionType: OptionType.Call,
|
|
163
180
|
strikePrice: 100_000,
|
|
@@ -166,11 +183,30 @@ const tx = await buildBuyFromPoolTransactionWithDerivation({
|
|
|
166
183
|
buyerPaymentAccount: buyerUsdcAta,
|
|
167
184
|
priceUpdate: pythPriceFeed,
|
|
168
185
|
quantity: 1_000_000,
|
|
169
|
-
|
|
186
|
+
quotedPremiumTotal: 50_000,
|
|
187
|
+
slippageBufferBaseUnits: 500_000n,
|
|
170
188
|
rpc,
|
|
171
189
|
});
|
|
172
190
|
```
|
|
173
191
|
|
|
192
|
+
### Buy premium semantics (market orders)
|
|
193
|
+
|
|
194
|
+
- `premiumAmount` / `max_premium_amount` is a **max premium cap**, not an exact premium target.
|
|
195
|
+
- Program computes premium on-chain at execution time and fails with `SlippageToleranceExceeded` if computed premium exceeds the cap.
|
|
196
|
+
- High-level market builder computes cap as `quotedPremiumTotal + buffer`:
|
|
197
|
+
- Canonical: `slippageBufferBaseUnits`
|
|
198
|
+
- Convenience for SOL/WSOL: `slippageBufferLamports`
|
|
199
|
+
- Default buffer: `500_000` base units (0.0005 SOL lamports)
|
|
200
|
+
|
|
201
|
+
### Buy liquidity errors (6041)
|
|
202
|
+
|
|
203
|
+
- `InsufficientPoolLiquidity` can happen when:
|
|
204
|
+
- `option_pool.total_available < quantity`, or
|
|
205
|
+
- remaining writer-position accounts cannot cover full quantity in the smallest-first fill loop.
|
|
206
|
+
- Recommended client flow:
|
|
207
|
+
1. Run `preflightBuyFromPoolMarketOrder` for UX gating.
|
|
208
|
+
2. Build via `buildBuyFromPoolMarketOrderTransactionWithDerivation` so pool + remaining accounts are refetched immediately before build.
|
|
209
|
+
|
|
174
210
|
### Unwind with loan repayment
|
|
175
211
|
|
|
176
212
|
```ts
|
package/client/lookup-table.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { PROGRAM_ID } from "./program";
|
|
|
4
4
|
import type { KitRpc } from "./types";
|
|
5
5
|
|
|
6
6
|
export const LOOKUP_TABLE_ADDRESSES: Record<"devnet" | "mainnet", Address | null> = {
|
|
7
|
-
devnet: address("
|
|
7
|
+
devnet: address("B7kctbAQDEHT8gJcqRB3Bjkv4EkxMCZecy2H6EQNLQPo"),
|
|
8
8
|
mainnet: null,
|
|
9
9
|
};
|
|
10
10
|
|
package/index.ts
CHANGED
package/long/builders.ts
CHANGED
|
@@ -10,14 +10,16 @@ import {
|
|
|
10
10
|
deriveAssociatedTokenAddress,
|
|
11
11
|
deriveBuyerPositionPda,
|
|
12
12
|
} from "../accounts/pdas";
|
|
13
|
-
import { assertPositiveAmount } from "../shared/amounts";
|
|
13
|
+
import { assertNonNegativeAmount, assertPositiveAmount } from "../shared/amounts";
|
|
14
14
|
import { invariant } from "../shared/errors";
|
|
15
15
|
import {
|
|
16
16
|
appendRemainingAccounts,
|
|
17
17
|
type RemainingAccountInput,
|
|
18
18
|
} from "../shared/remaining-accounts";
|
|
19
19
|
import type { OptionType } from "../generated/types";
|
|
20
|
-
import { getCreateAssociatedTokenIdempotentInstructionWithAddress } from "../wsol/instructions";
|
|
20
|
+
import { getCreateAssociatedTokenIdempotentInstructionWithAddress, NATIVE_MINT } from "../wsol/instructions";
|
|
21
|
+
import { fetchOptionPool } from "../accounts/fetchers";
|
|
22
|
+
import { getBuyFromPoolRemainingAccounts } from "./remaining-accounts";
|
|
21
23
|
|
|
22
24
|
export interface BuildBuyFromPoolParams {
|
|
23
25
|
optionPool: AddressLike;
|
|
@@ -127,6 +129,42 @@ export interface BuildBuyFromPoolTransactionWithDerivationParams {
|
|
|
127
129
|
remainingAccounts?: RemainingAccountInput[];
|
|
128
130
|
}
|
|
129
131
|
|
|
132
|
+
const DEFAULT_MARKET_ORDER_SLIPPAGE_BUFFER_BASE_UNITS = 500_000n;
|
|
133
|
+
|
|
134
|
+
interface MarketOrderBufferLikeParams {
|
|
135
|
+
slippageBufferBaseUnits?: bigint | number;
|
|
136
|
+
slippageBufferLamports?: bigint | number;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function normalizeMarketOrderSlippageBuffer(
|
|
140
|
+
params: MarketOrderBufferLikeParams,
|
|
141
|
+
underlyingMint: AddressLike
|
|
142
|
+
): bigint {
|
|
143
|
+
const hasBaseUnits = params.slippageBufferBaseUnits !== undefined;
|
|
144
|
+
const hasLamports = params.slippageBufferLamports !== undefined;
|
|
145
|
+
|
|
146
|
+
invariant(
|
|
147
|
+
!(hasBaseUnits && hasLamports),
|
|
148
|
+
"Provide only one of slippageBufferBaseUnits or slippageBufferLamports."
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
if (hasBaseUnits) {
|
|
152
|
+
assertNonNegativeAmount(params.slippageBufferBaseUnits!, "slippageBufferBaseUnits");
|
|
153
|
+
return BigInt(params.slippageBufferBaseUnits!);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (hasLamports) {
|
|
157
|
+
assertNonNegativeAmount(params.slippageBufferLamports!, "slippageBufferLamports");
|
|
158
|
+
invariant(
|
|
159
|
+
String(toAddress(underlyingMint)) === String(NATIVE_MINT),
|
|
160
|
+
"slippageBufferLamports is only supported for SOL/WSOL underlyings. Use slippageBufferBaseUnits for other assets."
|
|
161
|
+
);
|
|
162
|
+
return BigInt(params.slippageBufferLamports!);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return DEFAULT_MARKET_ORDER_SLIPPAGE_BUFFER_BASE_UNITS;
|
|
166
|
+
}
|
|
167
|
+
|
|
130
168
|
export async function buildBuyFromPoolTransactionWithDerivation(
|
|
131
169
|
params: BuildBuyFromPoolTransactionWithDerivationParams
|
|
132
170
|
): Promise<BuiltTransaction> {
|
|
@@ -178,6 +216,82 @@ export async function buildBuyFromPoolTransactionWithDerivation(
|
|
|
178
216
|
});
|
|
179
217
|
}
|
|
180
218
|
|
|
219
|
+
export interface BuildBuyFromPoolMarketOrderParams
|
|
220
|
+
extends Omit<
|
|
221
|
+
BuildBuyFromPoolTransactionWithDerivationParams,
|
|
222
|
+
"premiumAmount" | "remainingAccounts"
|
|
223
|
+
>,
|
|
224
|
+
MarketOrderBufferLikeParams {
|
|
225
|
+
quotedPremiumTotal: bigint | number;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* High-level market-order buy builder.
|
|
230
|
+
* Refetches option pool and remaining writer-position accounts right before
|
|
231
|
+
* build and sets max premium = quotedPremiumTotal + slippage buffer.
|
|
232
|
+
*/
|
|
233
|
+
export async function buildBuyFromPoolMarketOrderTransactionWithDerivation(
|
|
234
|
+
params: BuildBuyFromPoolMarketOrderParams
|
|
235
|
+
): Promise<BuiltTransaction> {
|
|
236
|
+
assertPositiveAmount(params.quantity, "quantity");
|
|
237
|
+
assertPositiveAmount(params.quotedPremiumTotal, "quotedPremiumTotal");
|
|
238
|
+
|
|
239
|
+
const resolved = await resolveOptionAccounts({
|
|
240
|
+
underlyingAsset: params.underlyingAsset,
|
|
241
|
+
optionType: params.optionType,
|
|
242
|
+
strikePrice: params.strikePrice,
|
|
243
|
+
expirationDate: params.expirationDate,
|
|
244
|
+
programId: params.programId,
|
|
245
|
+
rpc: params.rpc,
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
const [refetchedPool, remainingAccounts, buyerPosition, buyerOptionAccount] =
|
|
249
|
+
await Promise.all([
|
|
250
|
+
fetchOptionPool(params.rpc, resolved.optionPool),
|
|
251
|
+
getBuyFromPoolRemainingAccounts(params.rpc, resolved.optionPool, params.programId),
|
|
252
|
+
params.buyerPosition
|
|
253
|
+
? Promise.resolve(params.buyerPosition)
|
|
254
|
+
: deriveBuyerPositionPda(
|
|
255
|
+
params.buyer,
|
|
256
|
+
resolved.optionAccount,
|
|
257
|
+
params.programId
|
|
258
|
+
).then(([addr]) => addr),
|
|
259
|
+
params.buyerOptionAccount
|
|
260
|
+
? Promise.resolve(params.buyerOptionAccount)
|
|
261
|
+
: deriveAssociatedTokenAddress(params.buyer, resolved.longMint),
|
|
262
|
+
]);
|
|
263
|
+
|
|
264
|
+
invariant(
|
|
265
|
+
!!refetchedPool,
|
|
266
|
+
"Option pool must exist; ensure rpc is provided and pool is initialized."
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
const slippageBuffer = normalizeMarketOrderSlippageBuffer(
|
|
270
|
+
params,
|
|
271
|
+
refetchedPool.underlyingMint
|
|
272
|
+
);
|
|
273
|
+
const maxPremiumAmount = BigInt(params.quotedPremiumTotal) + slippageBuffer;
|
|
274
|
+
assertPositiveAmount(maxPremiumAmount, "maxPremiumAmount");
|
|
275
|
+
|
|
276
|
+
return buildBuyFromPoolTransaction({
|
|
277
|
+
optionPool: resolved.optionPool,
|
|
278
|
+
optionAccount: resolved.optionAccount,
|
|
279
|
+
longMint: resolved.longMint,
|
|
280
|
+
underlyingMint: refetchedPool.underlyingMint,
|
|
281
|
+
marketData: resolved.marketData,
|
|
282
|
+
priceUpdate: params.priceUpdate,
|
|
283
|
+
buyer: params.buyer,
|
|
284
|
+
buyerPaymentAccount: params.buyerPaymentAccount,
|
|
285
|
+
escrowLongAccount: refetchedPool.escrowLongAccount,
|
|
286
|
+
premiumVault: refetchedPool.premiumVault,
|
|
287
|
+
quantity: params.quantity,
|
|
288
|
+
premiumAmount: maxPremiumAmount,
|
|
289
|
+
buyerPosition,
|
|
290
|
+
buyerOptionAccount,
|
|
291
|
+
remainingAccounts,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
181
295
|
export async function buildCloseLongToPoolInstruction(
|
|
182
296
|
params: BuildCloseLongToPoolParams
|
|
183
297
|
): Promise<Instruction<string>> {
|
|
@@ -0,0 +1,114 @@
|
|
|
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
|
+
const availableWriterPositions = writerPositions.filter(
|
|
67
|
+
({ data }) => toBigInt(data.unsoldQty) > 0n
|
|
68
|
+
);
|
|
69
|
+
const remainingUnsoldAggregate = availableWriterPositions.reduce(
|
|
70
|
+
(acc, { data }) => acc + toBigInt(data.unsoldQty),
|
|
71
|
+
0n
|
|
72
|
+
);
|
|
73
|
+
const poolTotalAvailable = toBigInt(optionPool.totalAvailable);
|
|
74
|
+
|
|
75
|
+
const hasPoolLiquidity = poolTotalAvailable >= requestedQuantity;
|
|
76
|
+
const hasWriterCoverage = remainingUnsoldAggregate >= requestedQuantity;
|
|
77
|
+
|
|
78
|
+
let reason: string | undefined;
|
|
79
|
+
if (!hasPoolLiquidity) {
|
|
80
|
+
reason = "Pool total_available is less than requested quantity.";
|
|
81
|
+
} else if (!hasWriterCoverage) {
|
|
82
|
+
reason =
|
|
83
|
+
"Remaining writer-position liquidity is insufficient to fully fill requested quantity.";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const result: PreflightBuyFromPoolMarketOrderResult = {
|
|
87
|
+
canBuy: hasPoolLiquidity && hasWriterCoverage,
|
|
88
|
+
reason,
|
|
89
|
+
poolTotalAvailable,
|
|
90
|
+
requestedQuantity,
|
|
91
|
+
remainingAccountsCount: availableWriterPositions.length,
|
|
92
|
+
remainingUnsoldAggregate,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
if (params.quotedPremiumTotal !== undefined) {
|
|
96
|
+
assertPositiveAmount(params.quotedPremiumTotal, "quotedPremiumTotal");
|
|
97
|
+
if (params.slippageBufferBaseUnits !== undefined) {
|
|
98
|
+
assertNonNegativeAmount(params.slippageBufferBaseUnits, "slippageBufferBaseUnits");
|
|
99
|
+
}
|
|
100
|
+
const quotedPremiumTotal = toBigInt(params.quotedPremiumTotal);
|
|
101
|
+
const slippageBufferBaseUnits =
|
|
102
|
+
params.slippageBufferBaseUnits !== undefined
|
|
103
|
+
? toBigInt(params.slippageBufferBaseUnits)
|
|
104
|
+
: 0n;
|
|
105
|
+
|
|
106
|
+
result.premium = {
|
|
107
|
+
quotedPremiumTotal,
|
|
108
|
+
slippageBufferBaseUnits,
|
|
109
|
+
maxPremiumAmount: quotedPremiumTotal + slippageBufferBaseUnits,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return result;
|
|
114
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@epicentral/sos-sdk",
|
|
3
|
-
"version": "0.4.0-alpha.
|
|
3
|
+
"version": "0.4.0-alpha.2",
|
|
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",
|