@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
|
@@ -0,0 +1,619 @@
|
|
|
1
|
+
import { toAddress } from "../client/program";
|
|
2
|
+
import type { AddressLike, KitRpc } from "../client/types";
|
|
3
|
+
import type { OptionType } from "../generated";
|
|
4
|
+
import { fetchCollateralPool, fetchVault, fetchWriterPosition } from "../accounts/fetchers";
|
|
5
|
+
import { fetchPoolLoansByMaker } from "../accounts/list";
|
|
6
|
+
import { deriveAssociatedTokenAddress, deriveVaultPda, deriveWriterPositionPda } from "../accounts/pdas";
|
|
7
|
+
import { resolveOptionAccounts } from "../accounts/resolve-option";
|
|
8
|
+
import { invariant } from "../shared/errors";
|
|
9
|
+
|
|
10
|
+
const TOKEN_ACCOUNT_AMOUNT_OFFSET = 64;
|
|
11
|
+
const BPS_DENOMINATOR = 10_000n;
|
|
12
|
+
/** Fixed-point scale for pool accumulators (matches on-chain `FP_SCALE = 10^12`). */
|
|
13
|
+
const FP_SCALE = 1_000_000_000_000n;
|
|
14
|
+
/** Mainnet-beta slots per year, matches `SLOTS_PER_YEAR` in option-program. */
|
|
15
|
+
const SLOTS_PER_YEAR = 78_840_000n;
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
function readTokenAccountAmount(data: Uint8Array): bigint {
|
|
19
|
+
if (data.length < TOKEN_ACCOUNT_AMOUNT_OFFSET + 8) return 0n;
|
|
20
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
21
|
+
return view.getBigUint64(TOKEN_ACCOUNT_AMOUNT_OFFSET, true);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function fetchTokenAmount(rpc: KitRpc, tokenAccount: AddressLike): Promise<bigint> {
|
|
25
|
+
const response = await rpc.getAccountInfo(toAddress(tokenAccount), { encoding: "base64" }).send();
|
|
26
|
+
const info = response.value;
|
|
27
|
+
if (!info) return 0n;
|
|
28
|
+
const [base64Data] = info.data;
|
|
29
|
+
if (!base64Data) return 0n;
|
|
30
|
+
const decoded = atob(base64Data);
|
|
31
|
+
const bytes = new Uint8Array(decoded.length);
|
|
32
|
+
for (let i = 0; i < decoded.length; i++) bytes[i] = decoded.charCodeAt(i);
|
|
33
|
+
return readTokenAccountAmount(bytes);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function toBigInt(value: bigint | number): bigint {
|
|
37
|
+
return typeof value === "bigint" ? value : BigInt(value);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function minBigInt(...values: bigint[]): bigint {
|
|
41
|
+
if (values.length === 0) return 0n;
|
|
42
|
+
return values.reduce((a, b) => (a < b ? a : b));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* SPL balance can trail `proportionalCollateralShare` by a few base units
|
|
47
|
+
* (rounding / close-long debits vs `collateral_deposited`). Matches on-chain
|
|
48
|
+
* `collateral_vault_unwind_dust_epsilon` in option-program `pool.rs`.
|
|
49
|
+
*/
|
|
50
|
+
export function collateralVaultUnwindDustEpsilon(proportionalCollateralShare: bigint): bigint {
|
|
51
|
+
const fromPpm = (proportionalCollateralShare * 50n) / 1_000_000n;
|
|
52
|
+
const from10Bps = (proportionalCollateralShare * 10n) / 10_000n;
|
|
53
|
+
let a = fromPpm < 128_000n ? 128_000n : fromPpm;
|
|
54
|
+
const combined = a > from10Bps ? a : from10Bps;
|
|
55
|
+
return combined > 5_000_000n ? 5_000_000n : combined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* When the writer position reports no OMLP borrow state, do not treat unrelated
|
|
60
|
+
* active pool loan accounts (same maker+vault) as unwind repayment debt.
|
|
61
|
+
* Matches on-chain: repayment only runs when the writer still tracks active loans.
|
|
62
|
+
*/
|
|
63
|
+
export function writerPositionHasOngoingPoolLoanDebt(writerPosition: {
|
|
64
|
+
borrowedPrincipal: bigint | number;
|
|
65
|
+
activeLoanCount: number;
|
|
66
|
+
}): boolean {
|
|
67
|
+
return toBigInt(writerPosition.borrowedPrincipal) !== 0n || writerPosition.activeLoanCount > 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Client-side mirror of `WriterPosition::claimable_theta` + `theta_balance()`.
|
|
72
|
+
* Computes the theta balance that will be available on-chain after the
|
|
73
|
+
* instruction calls `realize_theta(pool.acc_theta_per_oi_fp)`.
|
|
74
|
+
*/
|
|
75
|
+
function computeThetaBalance(
|
|
76
|
+
writerPosition: {
|
|
77
|
+
soldQty: bigint | number;
|
|
78
|
+
thetaEarned: bigint | number;
|
|
79
|
+
thetaClaimed: bigint | number;
|
|
80
|
+
lastPoolAccThetaFp: bigint | number;
|
|
81
|
+
},
|
|
82
|
+
poolAccThetaFp: bigint | number
|
|
83
|
+
): bigint {
|
|
84
|
+
const accTheta = toBigInt(poolAccThetaFp);
|
|
85
|
+
const lastFp = toBigInt(writerPosition.lastPoolAccThetaFp);
|
|
86
|
+
const soldQty = toBigInt(writerPosition.soldQty);
|
|
87
|
+
const deltaFp = accTheta > lastFp ? accTheta - lastFp : 0n;
|
|
88
|
+
const pending = deltaFp > 0n && soldQty > 0n ? (soldQty * deltaFp) / FP_SCALE : 0n;
|
|
89
|
+
const earned = toBigInt(writerPosition.thetaEarned) + pending;
|
|
90
|
+
const claimed = toBigInt(writerPosition.thetaClaimed);
|
|
91
|
+
return earned > claimed ? earned - claimed : 0n;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** `(value * numerator) / denominator`, saturating at 0 when denominator is 0. */
|
|
95
|
+
function proportionalAmount(value: bigint, numerator: bigint, denominator: bigint): bigint {
|
|
96
|
+
if (denominator === 0n) return 0n;
|
|
97
|
+
return (value * numerator) / denominator;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface PreflightUnwindWriterUnsoldParams {
|
|
101
|
+
underlyingAsset: AddressLike;
|
|
102
|
+
optionType: OptionType;
|
|
103
|
+
strikePrice: number;
|
|
104
|
+
expirationDate: bigint | number;
|
|
105
|
+
writer: AddressLike;
|
|
106
|
+
unwindQty: bigint | number;
|
|
107
|
+
rpc: KitRpc;
|
|
108
|
+
programId?: AddressLike;
|
|
109
|
+
underlyingMint?: AddressLike;
|
|
110
|
+
/**
|
|
111
|
+
* @deprecated Wallet-based repayment is no longer supported under the
|
|
112
|
+
* theta-hedge model; unwind pays theta-first then proportional collateral,
|
|
113
|
+
* and reverts if the writer is insolvent (callers should route to the
|
|
114
|
+
* keeper rescue path instead).
|
|
115
|
+
*/
|
|
116
|
+
writerRepaymentAccount?: AddressLike;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface UnwindLoanBreakdown {
|
|
120
|
+
loanAddress: string;
|
|
121
|
+
principal: bigint;
|
|
122
|
+
accruedInterest: bigint;
|
|
123
|
+
accruedProtocolFees: bigint;
|
|
124
|
+
newlyAccruedInterest: bigint;
|
|
125
|
+
newlyAccruedProtocolFees: bigint;
|
|
126
|
+
totalInterest: bigint;
|
|
127
|
+
totalProtocolFees: bigint;
|
|
128
|
+
totalOwed: bigint;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export interface UnwindPreflightSummary {
|
|
132
|
+
activeLoanCount: number;
|
|
133
|
+
totalPrincipal: bigint;
|
|
134
|
+
totalInterest: bigint;
|
|
135
|
+
totalProtocolFees: bigint;
|
|
136
|
+
totalOwed: bigint;
|
|
137
|
+
/** Proportional obligations for partial unwind (based on unwind ratio). */
|
|
138
|
+
proportionalPrincipal: bigint;
|
|
139
|
+
proportionalInterest: bigint;
|
|
140
|
+
proportionalProtocolFees: bigint;
|
|
141
|
+
proportionalTotalOwed: bigint;
|
|
142
|
+
/** Proportional collateral claim the writer is owed if all debt is paid. */
|
|
143
|
+
proportionalCollateralShare: bigint;
|
|
144
|
+
/** Collateral returned to the writer after debt (pre-leftover-theta). */
|
|
145
|
+
returnableCollateral: bigint;
|
|
146
|
+
/** Total funds on hand in the collateral vault (SPL balance). */
|
|
147
|
+
collateralVaultAvailable: bigint;
|
|
148
|
+
/**
|
|
149
|
+
* Realized + pending theta available to offset debt on unwind.
|
|
150
|
+
* Mirrors `writer_position.realize_theta` followed by `theta_balance()`.
|
|
151
|
+
*/
|
|
152
|
+
thetaAvailable: bigint;
|
|
153
|
+
/**
|
|
154
|
+
* Portion of `thetaAvailable` used to repay the proportional debt.
|
|
155
|
+
* Zero when `premium_vault.mint != collateral_pool.collateral_mint`.
|
|
156
|
+
*/
|
|
157
|
+
thetaToDebt: bigint;
|
|
158
|
+
/** Residual debt after theta, paid from `collateral_vault`. */
|
|
159
|
+
collateralToDebt: bigint;
|
|
160
|
+
/** True when theta can offset debt (mint match with collateral pool). */
|
|
161
|
+
premiumMintMatchesCollateralMint: boolean;
|
|
162
|
+
/**
|
|
163
|
+
* Shortfall against the writer's proportional collateral claim; non-zero
|
|
164
|
+
* means the writer is underwater and the keeper rescue path is required.
|
|
165
|
+
*/
|
|
166
|
+
collateralClaimShortfall: bigint;
|
|
167
|
+
/**
|
|
168
|
+
* Shortfall between vault SPL balance and `proportionalCollateralShare` for
|
|
169
|
+
* this unwind slice, ignoring dust within `collateralVaultUnwindDustEpsilon`.
|
|
170
|
+
*/
|
|
171
|
+
collateralVaultShortfall: bigint;
|
|
172
|
+
/** True when the writer cannot fully repay the requested slice. */
|
|
173
|
+
needsRescue: boolean;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export interface UnwindPreflightResult {
|
|
177
|
+
canUnwind: boolean;
|
|
178
|
+
canRepayRequestedSlice: boolean;
|
|
179
|
+
/** @deprecated Use {@link canRepayRequestedSlice}. */
|
|
180
|
+
canRepayFully: boolean;
|
|
181
|
+
reason?: string;
|
|
182
|
+
/** Requested unwind quantity (from client). */
|
|
183
|
+
requestedUnwindQty: bigint;
|
|
184
|
+
/**
|
|
185
|
+
* Quantity used for repayment math and `unwind_writer_unsold` — `min` of requested, on-chain
|
|
186
|
+
* unsold, pool escrow LONG balance, and writer SHORT balance (matches program checks).
|
|
187
|
+
*/
|
|
188
|
+
effectiveUnwindQty: bigint;
|
|
189
|
+
writerPositionAddress: string;
|
|
190
|
+
/**
|
|
191
|
+
* @deprecated Under the theta-hedge model debt is repaid from theta +
|
|
192
|
+
* collateral vault; this field is retained for ABI compatibility and
|
|
193
|
+
* simply resolves to the writer's default underlying ATA.
|
|
194
|
+
*/
|
|
195
|
+
writerRepaymentAccount: string;
|
|
196
|
+
collateralVaultAddress: string;
|
|
197
|
+
loans: Array<UnwindLoanBreakdown>;
|
|
198
|
+
summary: UnwindPreflightSummary;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function emptySummary(): UnwindPreflightSummary {
|
|
202
|
+
return {
|
|
203
|
+
activeLoanCount: 0,
|
|
204
|
+
totalPrincipal: 0n,
|
|
205
|
+
totalInterest: 0n,
|
|
206
|
+
totalProtocolFees: 0n,
|
|
207
|
+
totalOwed: 0n,
|
|
208
|
+
proportionalPrincipal: 0n,
|
|
209
|
+
proportionalInterest: 0n,
|
|
210
|
+
proportionalProtocolFees: 0n,
|
|
211
|
+
proportionalTotalOwed: 0n,
|
|
212
|
+
proportionalCollateralShare: 0n,
|
|
213
|
+
returnableCollateral: 0n,
|
|
214
|
+
collateralVaultAvailable: 0n,
|
|
215
|
+
thetaAvailable: 0n,
|
|
216
|
+
thetaToDebt: 0n,
|
|
217
|
+
collateralToDebt: 0n,
|
|
218
|
+
premiumMintMatchesCollateralMint: false,
|
|
219
|
+
collateralClaimShortfall: 0n,
|
|
220
|
+
collateralVaultShortfall: 0n,
|
|
221
|
+
needsRescue: false,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export async function preflightUnwindWriterUnsold(
|
|
226
|
+
params: PreflightUnwindWriterUnsoldParams
|
|
227
|
+
): Promise<UnwindPreflightResult> {
|
|
228
|
+
const resolved = await resolveOptionAccounts({
|
|
229
|
+
underlyingAsset: params.underlyingAsset,
|
|
230
|
+
optionType: params.optionType,
|
|
231
|
+
strikePrice: params.strikePrice,
|
|
232
|
+
expirationDate: params.expirationDate,
|
|
233
|
+
programId: params.programId,
|
|
234
|
+
rpc: params.rpc,
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
invariant(
|
|
238
|
+
!!resolved.collateralVault && !!resolved.collateralPool && !!resolved.underlyingMint,
|
|
239
|
+
"Option/collateral pool state is required for unwind preflight."
|
|
240
|
+
);
|
|
241
|
+
invariant(
|
|
242
|
+
!!resolved.optionPoolData,
|
|
243
|
+
"Option pool state is required for unwind preflight (theta accumulator)."
|
|
244
|
+
);
|
|
245
|
+
invariant(
|
|
246
|
+
!!resolved.collateralPoolData,
|
|
247
|
+
"Collateral pool state is required for unwind preflight (collateral mint)."
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
const optionPoolData = resolved.optionPoolData;
|
|
251
|
+
const collateralPoolData = resolved.collateralPoolData;
|
|
252
|
+
|
|
253
|
+
const underlyingMint = params.underlyingMint ?? resolved.underlyingMint;
|
|
254
|
+
const [vaultPda] = await deriveVaultPda(underlyingMint, params.programId);
|
|
255
|
+
const vaultPdaAddress = toAddress(vaultPda);
|
|
256
|
+
const writerDefaultRepaymentAta = await deriveAssociatedTokenAddress(params.writer, underlyingMint);
|
|
257
|
+
// `writerRepaymentAccount` stays in the result for ABI compatibility with
|
|
258
|
+
// existing callers (e.g. wallet-fallback UX from pre-convergence), but it
|
|
259
|
+
// is not used on-chain anymore.
|
|
260
|
+
const writerRepaymentAddress = toAddress(
|
|
261
|
+
params.writerRepaymentAccount ?? writerDefaultRepaymentAta
|
|
262
|
+
);
|
|
263
|
+
const [writerPositionAddress] = await deriveWriterPositionPda(
|
|
264
|
+
resolved.optionPool,
|
|
265
|
+
params.writer,
|
|
266
|
+
params.programId
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
// #region agent log
|
|
270
|
+
const __debugIngest = (stage: string, data: Record<string, unknown>) => {
|
|
271
|
+
try {
|
|
272
|
+
fetch("http://127.0.0.1:7586/ingest/4a07cb33-954d-4b27-b1a3-08f1423b9d05", {
|
|
273
|
+
method: "POST",
|
|
274
|
+
headers: { "Content-Type": "application/json", "X-Debug-Session-Id": "af65cf" },
|
|
275
|
+
body: JSON.stringify({
|
|
276
|
+
sessionId: "af65cf",
|
|
277
|
+
location: "sos-sdk/short/preflight.ts:preflightUnwindWriterUnsold",
|
|
278
|
+
message: stage,
|
|
279
|
+
data,
|
|
280
|
+
timestamp: Date.now(),
|
|
281
|
+
hypothesisId: "H2-i64-writerPosition-or-poolLoan-size",
|
|
282
|
+
}),
|
|
283
|
+
}).catch(() => {});
|
|
284
|
+
} catch {}
|
|
285
|
+
};
|
|
286
|
+
// #endregion
|
|
287
|
+
// #region agent log
|
|
288
|
+
__debugIngest("before_account_fetch", {
|
|
289
|
+
writerPositionAddress: String(writerPositionAddress),
|
|
290
|
+
collateralPool: String(resolved.collateralPool),
|
|
291
|
+
vaultPda: String(vaultPda),
|
|
292
|
+
writer: String(params.writer),
|
|
293
|
+
});
|
|
294
|
+
// #endregion
|
|
295
|
+
|
|
296
|
+
let writerPosition: Awaited<ReturnType<typeof fetchWriterPosition>> | null = null;
|
|
297
|
+
let collateralPool: Awaited<ReturnType<typeof fetchCollateralPool>> | null = null;
|
|
298
|
+
let vault: Awaited<ReturnType<typeof fetchVault>> | null = null;
|
|
299
|
+
let loans: Awaited<ReturnType<typeof fetchPoolLoansByMaker>> = [];
|
|
300
|
+
let currentSlot: bigint | number = 0n;
|
|
301
|
+
try {
|
|
302
|
+
writerPosition = await fetchWriterPosition(params.rpc, writerPositionAddress);
|
|
303
|
+
// #region agent log
|
|
304
|
+
__debugIngest("fetchWriterPosition_ok", { hasPosition: !!writerPosition });
|
|
305
|
+
// #endregion
|
|
306
|
+
} catch (e) {
|
|
307
|
+
// #region agent log
|
|
308
|
+
__debugIngest("fetchWriterPosition_throw", {
|
|
309
|
+
err: (e as Error)?.message ?? String(e),
|
|
310
|
+
stack: String((e as Error)?.stack ?? "").slice(0, 600),
|
|
311
|
+
});
|
|
312
|
+
// #endregion
|
|
313
|
+
throw e;
|
|
314
|
+
}
|
|
315
|
+
try {
|
|
316
|
+
collateralPool = await fetchCollateralPool(params.rpc, resolved.collateralPool);
|
|
317
|
+
} catch (e) {
|
|
318
|
+
// #region agent log
|
|
319
|
+
__debugIngest("fetchCollateralPool_throw", { err: (e as Error)?.message ?? String(e) });
|
|
320
|
+
// #endregion
|
|
321
|
+
throw e;
|
|
322
|
+
}
|
|
323
|
+
try {
|
|
324
|
+
vault = await fetchVault(params.rpc, vaultPda);
|
|
325
|
+
} catch (e) {
|
|
326
|
+
// #region agent log
|
|
327
|
+
__debugIngest("fetchVault_throw", { err: (e as Error)?.message ?? String(e) });
|
|
328
|
+
// #endregion
|
|
329
|
+
throw e;
|
|
330
|
+
}
|
|
331
|
+
try {
|
|
332
|
+
loans = await fetchPoolLoansByMaker(params.rpc, params.writer);
|
|
333
|
+
// #region agent log
|
|
334
|
+
__debugIngest("fetchPoolLoansByMaker_ok", { count: loans.length });
|
|
335
|
+
// #endregion
|
|
336
|
+
} catch (e) {
|
|
337
|
+
// #region agent log
|
|
338
|
+
__debugIngest("fetchPoolLoansByMaker_throw", {
|
|
339
|
+
err: (e as Error)?.message ?? String(e),
|
|
340
|
+
stack: String((e as Error)?.stack ?? "").slice(0, 600),
|
|
341
|
+
});
|
|
342
|
+
// #endregion
|
|
343
|
+
throw e;
|
|
344
|
+
}
|
|
345
|
+
currentSlot = await params.rpc.getSlot().send();
|
|
346
|
+
|
|
347
|
+
invariant(!!writerPosition, "Writer position is required for unwind preflight.");
|
|
348
|
+
invariant(!!collateralPool, "Collateral pool is required for unwind preflight.");
|
|
349
|
+
invariant(!!vault, "Vault state is required for unwind preflight.");
|
|
350
|
+
invariant(
|
|
351
|
+
!!resolved.escrowLongAccount && !!resolved.shortMint,
|
|
352
|
+
"Option pool escrow LONG and short mint are required for unwind preflight."
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
const writerShortAta = await deriveAssociatedTokenAddress(params.writer, resolved.shortMint);
|
|
356
|
+
const [escrowLongBal, writerShortBal] = await Promise.all([
|
|
357
|
+
fetchTokenAmount(params.rpc, resolved.escrowLongAccount),
|
|
358
|
+
fetchTokenAmount(params.rpc, writerShortAta),
|
|
359
|
+
]);
|
|
360
|
+
|
|
361
|
+
const unwindQtyRequested = toBigInt(params.unwindQty);
|
|
362
|
+
const unsoldQty = toBigInt(writerPosition.unsoldQty);
|
|
363
|
+
const unwindQtyEffective = minBigInt(unwindQtyRequested, unsoldQty, escrowLongBal, writerShortBal);
|
|
364
|
+
|
|
365
|
+
const baseResult = (
|
|
366
|
+
canUnwind: boolean,
|
|
367
|
+
canRepayRequestedSlice: boolean,
|
|
368
|
+
reason: string | undefined,
|
|
369
|
+
effectiveQty: bigint
|
|
370
|
+
): UnwindPreflightResult => ({
|
|
371
|
+
canUnwind,
|
|
372
|
+
canRepayRequestedSlice,
|
|
373
|
+
canRepayFully: canRepayRequestedSlice,
|
|
374
|
+
reason,
|
|
375
|
+
requestedUnwindQty: unwindQtyRequested,
|
|
376
|
+
effectiveUnwindQty: effectiveQty,
|
|
377
|
+
writerPositionAddress: String(writerPositionAddress),
|
|
378
|
+
writerRepaymentAccount: String(writerRepaymentAddress),
|
|
379
|
+
collateralVaultAddress: String(resolved.collateralVault),
|
|
380
|
+
loans: [],
|
|
381
|
+
summary: emptySummary(),
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
if (unwindQtyRequested <= 0n) {
|
|
385
|
+
return baseResult(false, false, "unwindQty must be > 0", 0n);
|
|
386
|
+
}
|
|
387
|
+
if (unwindQtyRequested > unsoldQty) {
|
|
388
|
+
return baseResult(false, false, "unwindQty exceeds writer unsold quantity", 0n);
|
|
389
|
+
}
|
|
390
|
+
if (unwindQtyEffective <= 0n) {
|
|
391
|
+
return baseResult(
|
|
392
|
+
false,
|
|
393
|
+
false,
|
|
394
|
+
"Pool escrow LONG balance or your SHORT token balance is insufficient for any unwind. Refresh positions or unwind a smaller amount.",
|
|
395
|
+
0n
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const unwindQty = unwindQtyEffective;
|
|
400
|
+
|
|
401
|
+
const slotNow = toBigInt(currentSlot);
|
|
402
|
+
const protocolFeeBps = BigInt(vault.protocolFeeBps);
|
|
403
|
+
const loanBreakdown: Array<UnwindLoanBreakdown> = [];
|
|
404
|
+
|
|
405
|
+
const includePoolLoansForRepay = writerPositionHasOngoingPoolLoanDebt(writerPosition);
|
|
406
|
+
|
|
407
|
+
const vaultLoanCandidates = loans.filter(
|
|
408
|
+
(loan) => toAddress(loan.data.vault) === vaultPdaAddress && Number(loan.data.status) === 1
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
const writerPositionAddr = String(writerPositionAddress);
|
|
412
|
+
const matchingLoans = includePoolLoansForRepay
|
|
413
|
+
? vaultLoanCandidates.filter(
|
|
414
|
+
(loan) => toAddress(loan.data.writerPosition) === writerPositionAddr
|
|
415
|
+
)
|
|
416
|
+
: [];
|
|
417
|
+
|
|
418
|
+
// The on-chain `sync_collateral_pool_debt` requires the writer-position-matching loans in
|
|
419
|
+
// `remaining_accounts` to be exactly `writer.active_loan_count`. Dev clusters can leave
|
|
420
|
+
// orphan PoolLoan PDAs with status==1 not tracked by the writer, so when matchingLoans >
|
|
421
|
+
// activeLoanCount we sort by nonce DESC and take the top-N (most recent borrows are the
|
|
422
|
+
// ones the writer accumulator tracks), then verify the principal sum matches
|
|
423
|
+
// writer.borrowed_principal as a safety check.
|
|
424
|
+
const sortByNonceDesc = (
|
|
425
|
+
a: (typeof matchingLoans)[number],
|
|
426
|
+
b: (typeof matchingLoans)[number]
|
|
427
|
+
) => {
|
|
428
|
+
if (a.data.nonce > b.data.nonce) return -1;
|
|
429
|
+
if (a.data.nonce < b.data.nonce) return 1;
|
|
430
|
+
return String(a.address).localeCompare(String(b.address));
|
|
431
|
+
};
|
|
432
|
+
const positionLoans = includePoolLoansForRepay
|
|
433
|
+
? matchingLoans.slice().sort(sortByNonceDesc).slice(0, writerPosition.activeLoanCount)
|
|
434
|
+
: [];
|
|
435
|
+
|
|
436
|
+
if (includePoolLoansForRepay) {
|
|
437
|
+
// #region agent log
|
|
438
|
+
__debugIngest("loan_selection", {
|
|
439
|
+
activeLoanCount: writerPosition.activeLoanCount,
|
|
440
|
+
matchingLoansFound: matchingLoans.length,
|
|
441
|
+
selectedCount: positionLoans.length,
|
|
442
|
+
selectedNonces: positionLoans.map((l) => Number(l.data.nonce)),
|
|
443
|
+
selectedPrincipalSum: positionLoans
|
|
444
|
+
.reduce((s, l) => s + toBigInt(l.data.principal), 0n)
|
|
445
|
+
.toString(),
|
|
446
|
+
writerBorrowedPrincipal: toBigInt(writerPosition.borrowedPrincipal).toString(),
|
|
447
|
+
});
|
|
448
|
+
// #endregion
|
|
449
|
+
if (positionLoans.length !== writerPosition.activeLoanCount) {
|
|
450
|
+
return baseResult(
|
|
451
|
+
false,
|
|
452
|
+
false,
|
|
453
|
+
`Pool loan set mismatch: writer tracks ${writerPosition.activeLoanCount} active loan(s) but only found ${matchingLoans.length} matching status==1 PoolLoan(s) for this writer position. Refresh positions.`,
|
|
454
|
+
unwindQtyEffective
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
const selectedPrincipalSum = positionLoans.reduce(
|
|
458
|
+
(sum, loan) => sum + toBigInt(loan.data.principal),
|
|
459
|
+
0n
|
|
460
|
+
);
|
|
461
|
+
const writerBorrowedPrincipal = toBigInt(writerPosition.borrowedPrincipal);
|
|
462
|
+
if (selectedPrincipalSum !== writerBorrowedPrincipal) {
|
|
463
|
+
return baseResult(
|
|
464
|
+
false,
|
|
465
|
+
false,
|
|
466
|
+
`Active pool loan principal mismatch: top-${writerPosition.activeLoanCount} loans by nonce sum to ${selectedPrincipalSum} but writer.borrowed_principal=${writerBorrowedPrincipal}. On-chain state is inconsistent (extra orphan PoolLoans for this writer). Use the keeper rescue path.`,
|
|
467
|
+
unwindQtyEffective
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (includePoolLoansForRepay) {
|
|
473
|
+
for (const loan of positionLoans) {
|
|
474
|
+
const principal = toBigInt(loan.data.principal);
|
|
475
|
+
const accruedInterest = toBigInt(loan.data.accruedInterest);
|
|
476
|
+
const accruedProtocolFees = toBigInt(loan.data.accruedProtocolFees);
|
|
477
|
+
const rateBps = BigInt(loan.data.rateBps);
|
|
478
|
+
const lastUpdateSlot = toBigInt(loan.data.lastUpdateSlot);
|
|
479
|
+
const slotsElapsed = slotNow > lastUpdateSlot ? slotNow - lastUpdateSlot : 0n;
|
|
480
|
+
const newlyAccruedInterest =
|
|
481
|
+
slotsElapsed > 0n ? (principal * rateBps * slotsElapsed) / BPS_DENOMINATOR / SLOTS_PER_YEAR : 0n;
|
|
482
|
+
const newlyAccruedProtocolFees =
|
|
483
|
+
slotsElapsed > 0n
|
|
484
|
+
? (principal * protocolFeeBps * slotsElapsed) / BPS_DENOMINATOR / SLOTS_PER_YEAR
|
|
485
|
+
: 0n;
|
|
486
|
+
const totalInterest = accruedInterest + newlyAccruedInterest;
|
|
487
|
+
const totalProtocolFees = accruedProtocolFees + newlyAccruedProtocolFees;
|
|
488
|
+
const totalOwed = principal + totalInterest + totalProtocolFees;
|
|
489
|
+
|
|
490
|
+
loanBreakdown.push({
|
|
491
|
+
loanAddress: String(loan.address),
|
|
492
|
+
principal,
|
|
493
|
+
accruedInterest,
|
|
494
|
+
accruedProtocolFees,
|
|
495
|
+
newlyAccruedInterest,
|
|
496
|
+
newlyAccruedProtocolFees,
|
|
497
|
+
totalInterest,
|
|
498
|
+
totalProtocolFees,
|
|
499
|
+
totalOwed,
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const totals = loanBreakdown.reduce(
|
|
505
|
+
(acc, item) => ({
|
|
506
|
+
principal: acc.principal + item.principal,
|
|
507
|
+
interest: acc.interest + item.totalInterest,
|
|
508
|
+
fees: acc.fees + item.totalProtocolFees,
|
|
509
|
+
owed: acc.owed + item.totalOwed,
|
|
510
|
+
}),
|
|
511
|
+
{ principal: 0n, interest: 0n, fees: 0n, owed: 0n }
|
|
512
|
+
);
|
|
513
|
+
|
|
514
|
+
const collateralVaultAvailable = await fetchTokenAmount(
|
|
515
|
+
params.rpc,
|
|
516
|
+
resolved.collateralVault!
|
|
517
|
+
);
|
|
518
|
+
|
|
519
|
+
const writtenQty = toBigInt(writerPosition.writtenQty);
|
|
520
|
+
const proportionalPrincipal = proportionalAmount(totals.principal, unwindQty, writtenQty);
|
|
521
|
+
const proportionalInterest = proportionalAmount(totals.interest, unwindQty, writtenQty);
|
|
522
|
+
const proportionalProtocolFees = proportionalAmount(totals.fees, unwindQty, writtenQty);
|
|
523
|
+
const proportionalTotalOwed =
|
|
524
|
+
proportionalPrincipal + proportionalInterest + proportionalProtocolFees;
|
|
525
|
+
|
|
526
|
+
const collateralDeposited = toBigInt(writerPosition.collateralDeposited);
|
|
527
|
+
const proportionalCollateralShare = proportionalAmount(
|
|
528
|
+
collateralDeposited,
|
|
529
|
+
unwindQty,
|
|
530
|
+
writtenQty
|
|
531
|
+
);
|
|
532
|
+
|
|
533
|
+
// Theta balance after realize_theta(pool.acc_theta_per_oi_fp).
|
|
534
|
+
const thetaAvailable = computeThetaBalance(writerPosition, optionPoolData.accThetaPerOiFp);
|
|
535
|
+
|
|
536
|
+
// Theta can only offset debt when premium_vault.mint == omlp_vault.mint.
|
|
537
|
+
// In OPX, premium_vault.mint == option_pool.underlying_mint (enforced via
|
|
538
|
+
// `buyer_payment_account.mint == option_pool.underlying_mint`), and the
|
|
539
|
+
// OMLP vault is derived for the collateral mint. They match when the
|
|
540
|
+
// collateral pool's collateral_mint equals the option pool's underlying_mint.
|
|
541
|
+
const premiumMintMatchesCollateralMint =
|
|
542
|
+
toAddress(optionPoolData.underlyingMint) === toAddress(collateralPoolData.collateralMint);
|
|
543
|
+
|
|
544
|
+
const thetaToDebt = premiumMintMatchesCollateralMint
|
|
545
|
+
? minBigInt(thetaAvailable, proportionalTotalOwed)
|
|
546
|
+
: 0n;
|
|
547
|
+
const collateralToDebt = proportionalTotalOwed - thetaToDebt;
|
|
548
|
+
|
|
549
|
+
// Full-repay-or-revert: writer's proportional collateral share MUST cover
|
|
550
|
+
// the residual debt after theta. A non-zero `collateralClaimShortfall`
|
|
551
|
+
// means the writer is insolvent on this slice and the keeper rescue path
|
|
552
|
+
// is required.
|
|
553
|
+
const collateralClaimShortfall =
|
|
554
|
+
collateralToDebt > proportionalCollateralShare
|
|
555
|
+
? collateralToDebt - proportionalCollateralShare
|
|
556
|
+
: 0n;
|
|
557
|
+
// On-chain, `collateral_vault` pays `collateralToDebt` then returns
|
|
558
|
+
// `proportionalCollateralShare - collateralToDebt` to the writer — total
|
|
559
|
+
// `proportionalCollateralShare`. Ignore gaps ≤ dust epsilon (see
|
|
560
|
+
// `collateralVaultUnwindDustEpsilon`).
|
|
561
|
+
const rawCollateralVaultShortfall =
|
|
562
|
+
proportionalCollateralShare > collateralVaultAvailable
|
|
563
|
+
? proportionalCollateralShare - collateralVaultAvailable
|
|
564
|
+
: 0n;
|
|
565
|
+
const vaultDustEps = collateralVaultUnwindDustEpsilon(proportionalCollateralShare);
|
|
566
|
+
const collateralVaultShortfall =
|
|
567
|
+
rawCollateralVaultShortfall > vaultDustEps ? rawCollateralVaultShortfall : 0n;
|
|
568
|
+
|
|
569
|
+
const returnableCollateral =
|
|
570
|
+
proportionalCollateralShare > collateralToDebt
|
|
571
|
+
? proportionalCollateralShare - collateralToDebt
|
|
572
|
+
: 0n;
|
|
573
|
+
|
|
574
|
+
const needsRescue = collateralClaimShortfall > 0n || collateralVaultShortfall > 0n;
|
|
575
|
+
const canRepayRequestedSlice = !needsRescue;
|
|
576
|
+
|
|
577
|
+
let reason: string | undefined;
|
|
578
|
+
if (collateralClaimShortfall > 0n) {
|
|
579
|
+
reason =
|
|
580
|
+
"Writer position is underwater: realized theta + proportional collateral cannot repay the debt slice. Call the keeper rescue path (liquidate_writer_position_rescue).";
|
|
581
|
+
} else if (collateralVaultShortfall > 0n) {
|
|
582
|
+
reason =
|
|
583
|
+
"Collateral vault is too drained to settle this slice. Refresh positions and retry, or call the keeper rescue path.";
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
return {
|
|
587
|
+
canUnwind: true,
|
|
588
|
+
canRepayRequestedSlice,
|
|
589
|
+
canRepayFully: canRepayRequestedSlice,
|
|
590
|
+
reason,
|
|
591
|
+
requestedUnwindQty: unwindQtyRequested,
|
|
592
|
+
effectiveUnwindQty: unwindQtyEffective,
|
|
593
|
+
writerPositionAddress: String(writerPositionAddress),
|
|
594
|
+
writerRepaymentAccount: String(writerRepaymentAddress),
|
|
595
|
+
collateralVaultAddress: String(resolved.collateralVault),
|
|
596
|
+
loans: loanBreakdown,
|
|
597
|
+
summary: {
|
|
598
|
+
activeLoanCount: loanBreakdown.length,
|
|
599
|
+
totalPrincipal: totals.principal,
|
|
600
|
+
totalInterest: totals.interest,
|
|
601
|
+
totalProtocolFees: totals.fees,
|
|
602
|
+
totalOwed: totals.owed,
|
|
603
|
+
proportionalPrincipal,
|
|
604
|
+
proportionalInterest,
|
|
605
|
+
proportionalProtocolFees,
|
|
606
|
+
proportionalTotalOwed,
|
|
607
|
+
proportionalCollateralShare,
|
|
608
|
+
returnableCollateral,
|
|
609
|
+
collateralVaultAvailable,
|
|
610
|
+
thetaAvailable,
|
|
611
|
+
thetaToDebt,
|
|
612
|
+
collateralToDebt,
|
|
613
|
+
premiumMintMatchesCollateralMint,
|
|
614
|
+
collateralClaimShortfall,
|
|
615
|
+
collateralVaultShortfall,
|
|
616
|
+
needsRescue,
|
|
617
|
+
},
|
|
618
|
+
};
|
|
619
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"module": "es2022",
|
|
4
|
+
"target": "es2020",
|
|
5
|
+
"moduleResolution": "node",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"declaration": true,
|
|
8
|
+
"outDir": "dist",
|
|
9
|
+
"skipLibCheck": true
|
|
10
|
+
},
|
|
11
|
+
"include": ["**/*.ts"],
|
|
12
|
+
"exclude": ["dist", "node_modules"]
|
|
13
|
+
}
|