@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.
Files changed (113) hide show
  1. package/.env.example +1 -0
  2. package/AGENTS.md +7 -0
  3. package/LICENSE +21 -0
  4. package/README.md +568 -0
  5. package/accounts/fetchers.ts +196 -0
  6. package/accounts/list.ts +184 -0
  7. package/accounts/pdas.ts +325 -0
  8. package/accounts/resolve-option.ts +104 -0
  9. package/client/lookup-table.ts +114 -0
  10. package/client/program.ts +13 -0
  11. package/client/types.ts +9 -0
  12. package/generated/accounts/collateralPool.ts +217 -0
  13. package/generated/accounts/config.ts +156 -0
  14. package/generated/accounts/escrowState.ts +183 -0
  15. package/generated/accounts/index.ts +20 -0
  16. package/generated/accounts/lenderPosition.ts +211 -0
  17. package/generated/accounts/makerCollateralShare.ts +229 -0
  18. package/generated/accounts/marketDataAccount.ts +176 -0
  19. package/generated/accounts/optionAccount.ts +247 -0
  20. package/generated/accounts/optionPool.ts +285 -0
  21. package/generated/accounts/poolLoan.ts +232 -0
  22. package/generated/accounts/positionAccount.ts +201 -0
  23. package/generated/accounts/vault.ts +366 -0
  24. package/generated/accounts/writerPosition.ts +327 -0
  25. package/generated/errors/index.ts +9 -0
  26. package/generated/errors/optionProgram.ts +476 -0
  27. package/generated/index.ts +13 -0
  28. package/generated/instructions/acceptAdmin.ts +230 -0
  29. package/generated/instructions/autoExerciseAllExpired.ts +685 -0
  30. package/generated/instructions/autoExerciseExpired.ts +754 -0
  31. package/generated/instructions/borrowFromPool.ts +619 -0
  32. package/generated/instructions/buyFromPool.ts +761 -0
  33. package/generated/instructions/closeLongToPool.ts +762 -0
  34. package/generated/instructions/closeOption.ts +235 -0
  35. package/generated/instructions/createEscrowV2.ts +518 -0
  36. package/generated/instructions/depositCollateral.ts +624 -0
  37. package/generated/instructions/depositToPosition.ts +429 -0
  38. package/generated/instructions/index.ts +47 -0
  39. package/generated/instructions/initCollateralPool.ts +513 -0
  40. package/generated/instructions/initConfig.ts +279 -0
  41. package/generated/instructions/initOptionPool.ts +587 -0
  42. package/generated/instructions/initializeMarketData.ts +359 -0
  43. package/generated/instructions/liquidateWriterPosition.ts +750 -0
  44. package/generated/instructions/liquidateWriterPositionRescue.ts +623 -0
  45. package/generated/instructions/omlpCreateVault.ts +553 -0
  46. package/generated/instructions/omlpUpdateFeeWallet.ts +473 -0
  47. package/generated/instructions/omlpUpdateInterestModel.ts +322 -0
  48. package/generated/instructions/omlpUpdateLiquidationThreshold.ts +304 -0
  49. package/generated/instructions/omlpUpdateMaintenanceBuffer.ts +304 -0
  50. package/generated/instructions/omlpUpdateMaxBorrowCap.ts +304 -0
  51. package/generated/instructions/omlpUpdateMaxLeverage.ts +304 -0
  52. package/generated/instructions/omlpUpdateProtocolFee.ts +304 -0
  53. package/generated/instructions/omlpUpdateSupplyLimit.ts +304 -0
  54. package/generated/instructions/optionExercise.ts +617 -0
  55. package/generated/instructions/optionMint.ts +1373 -0
  56. package/generated/instructions/optionValidate.ts +302 -0
  57. package/generated/instructions/repayPoolLoan.ts +558 -0
  58. package/generated/instructions/repayPoolLoanFromCollateral.ts +514 -0
  59. package/generated/instructions/repayPoolLoanFromWallet.ts +542 -0
  60. package/generated/instructions/settleMakerCollateral.ts +509 -0
  61. package/generated/instructions/syncWriterPosition.ts +206 -0
  62. package/generated/instructions/transferAdmin.ts +245 -0
  63. package/generated/instructions/unwindWriterUnsold.ts +764 -0
  64. package/generated/instructions/updateImpliedVolatility.ts +226 -0
  65. package/generated/instructions/updateMarketData.ts +315 -0
  66. package/generated/instructions/withdrawFromPosition.ts +405 -0
  67. package/generated/instructions/writeToPool.ts +619 -0
  68. package/generated/programs/index.ts +9 -0
  69. package/generated/programs/optionProgram.ts +1144 -0
  70. package/generated/shared/index.ts +164 -0
  71. package/generated/types/impliedVolatilityUpdated.ts +73 -0
  72. package/generated/types/index.ts +28 -0
  73. package/generated/types/liquidationExecuted.ts +73 -0
  74. package/generated/types/liquidationRescueEvent.ts +82 -0
  75. package/generated/types/marketDataInitialized.ts +61 -0
  76. package/generated/types/marketDataUpdated.ts +69 -0
  77. package/generated/types/optionClosed.ts +56 -0
  78. package/generated/types/optionExercised.ts +62 -0
  79. package/generated/types/optionExpired.ts +49 -0
  80. package/generated/types/optionMinted.ts +78 -0
  81. package/generated/types/optionType.ts +38 -0
  82. package/generated/types/optionValidated.ts +82 -0
  83. package/generated/types/poolLoanCreated.ts +74 -0
  84. package/generated/types/poolLoanRepaid.ts +74 -0
  85. package/generated/types/positionDeposited.ts +73 -0
  86. package/generated/types/positionWithdrawn.ts +81 -0
  87. package/generated/types/protocolFeeUpdated.ts +69 -0
  88. package/generated/types/vaultCreated.ts +60 -0
  89. package/generated/types/vaultFeeWalletUpdated.ts +67 -0
  90. package/generated/types/vaultInterestModelUpdated.ts +77 -0
  91. package/generated/types/vaultLiquidationThresholdUpdated.ts +69 -0
  92. package/index.ts +68 -0
  93. package/long/builders.ts +690 -0
  94. package/long/exercise.ts +123 -0
  95. package/long/preflight.ts +214 -0
  96. package/long/quotes.ts +48 -0
  97. package/long/remaining-accounts.ts +111 -0
  98. package/omlp/builders.ts +94 -0
  99. package/omlp/service.ts +136 -0
  100. package/oracle/switchboard.ts +315 -0
  101. package/package.json +34 -0
  102. package/shared/amounts.ts +53 -0
  103. package/shared/balances.ts +57 -0
  104. package/shared/errors.ts +12 -0
  105. package/shared/remaining-accounts.ts +41 -0
  106. package/shared/trade-config.ts +27 -0
  107. package/shared/transactions.ts +121 -0
  108. package/short/builders.ts +874 -0
  109. package/short/close-option.ts +34 -0
  110. package/short/pool.ts +189 -0
  111. package/short/preflight.ts +619 -0
  112. package/tsconfig.json +13 -0
  113. package/wsol/instructions.ts +247 -0
@@ -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
+ }
@@ -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
+ }