perpcity_sdk/types.rs
1//! Client-facing types for the PerpCity SDK.
2//!
3//! These types use `f64` for human-readable values (prices, USDC amounts,
4//! leverage) and Alloy's [`Address`] / [`B256`] for on-chain identifiers.
5//! They are the public API surface — users construct these, and the SDK
6//! converts them to wire-format contract types internally.
7//!
8//! All types implement [`Serialize`] and
9//! [`Deserialize`] for logging, dashboards, persistence,
10//! and inter-process communication.
11
12use alloy::primitives::{Address, B256, U256};
13use serde::{Deserialize, Serialize};
14
15/// Deployed contract addresses for a PerpCity instance.
16#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
17pub struct Deployments {
18 /// PerpManager proxy address.
19 pub perp_manager: Address,
20 /// USDC token address.
21 pub usdc: Address,
22 /// Fees module address (if registered).
23 pub fees_module: Option<Address>,
24 /// Margin ratios module address (if registered).
25 pub margin_ratios_module: Option<Address>,
26 /// Lockup period module address (if registered).
27 pub lockup_period_module: Option<Address>,
28 /// Sqrt-price impact limit module address (if registered).
29 pub sqrt_price_impact_limit_module: Option<Address>,
30}
31
32/// Metadata about a perpetual market.
33#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
34pub struct PerpData {
35 /// Unique perp identifier (`PoolId` / `bytes32` on-chain).
36 pub id: B256,
37 /// Tick spacing for the underlying Uniswap V4 pool.
38 pub tick_spacing: i32,
39 /// Current mark price in human-readable units (e.g. `1.05`).
40 pub mark: f64,
41 /// Beacon contract address.
42 pub beacon: Address,
43 /// Leverage and margin constraints.
44 pub bounds: Bounds,
45 /// Fee structure.
46 pub fees: Fees,
47}
48
49/// Leverage and margin constraints for a perpetual market.
50///
51/// All values are human-readable: leverage as a multiplier (e.g. `10.0`),
52/// margin in USDC (e.g. `5.0`), and ratios as fractions (e.g. `0.005`).
53#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
54pub struct Bounds {
55 /// Minimum margin to open a position, in USDC (e.g. `5.0`).
56 pub min_margin: f64,
57 /// Minimum taker leverage (e.g. `1.0`).
58 pub min_taker_leverage: f64,
59 /// Maximum taker leverage (e.g. `100.0`).
60 pub max_taker_leverage: f64,
61 /// Margin ratio at which taker liquidation occurs, as a fraction
62 /// (e.g. `0.005` = 0.5%).
63 pub liquidation_taker_ratio: f64,
64}
65
66/// Fee percentages for a perpetual market, expressed as fractions of 1.
67///
68/// For example, `0.001` means 0.1% (which is `1_000` on-chain at 1e6 scale).
69#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
70pub struct Fees {
71 /// Fee paid to the perp creator.
72 pub creator_fee: f64,
73 /// Fee that goes to the insurance fund.
74 pub insurance_fee: f64,
75 /// Fee earned by liquidity providers.
76 pub lp_fee: f64,
77 /// Fee charged on liquidations.
78 pub liquidation_fee: f64,
79}
80
81/// Real-time position metrics, typically from a `quoteClosePosition` call.
82///
83/// All USDC values are human-readable (e.g. `12.50` not `12_500_000`).
84#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
85pub struct LiveDetails {
86 /// Unrealized PnL in USDC.
87 pub pnl: f64,
88 /// Accumulated funding payment in USDC (positive = received).
89 pub funding_payment: f64,
90 /// Current effective margin in USDC.
91 pub effective_margin: f64,
92 /// Whether this position would be liquidated at the current price.
93 pub is_liquidatable: bool,
94}
95
96/// Taker open interest for a perp market, in USDC.
97#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
98pub struct OpenInterest {
99 /// Total long open interest in USDC.
100 pub long_oi: f64,
101 /// Total short open interest in USDC.
102 pub short_oi: f64,
103}
104
105/// Live market data from a multicall snapshot.
106///
107/// Pure market state — no static config. Returned alongside [`PerpData`]
108/// from [`PerpClient::get_perp_snapshot`](crate::PerpClient::get_perp_snapshot).
109#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
110pub struct PerpSnapshot {
111 /// Current mark price (TWAP) in human-readable units.
112 pub mark_price: f64,
113 /// Oracle index price from the beacon contract.
114 pub index_price: f64,
115 /// Daily funding rate (positive = longs pay shorts).
116 pub funding_rate_daily: f64,
117 /// Taker open interest.
118 pub open_interest: OpenInterest,
119}
120
121/// Client-facing parameters for opening a taker (long/short) position.
122///
123/// The SDK converts these to contract types automatically:
124/// - `margin` → scaled to 6 decimals
125/// - `leverage` → converted to margin ratio via `1e6 / leverage`
126#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
127pub struct OpenTakerParams {
128 /// `true` for long, `false` for short.
129 pub is_long: bool,
130 /// Margin in USDC (e.g. `100.0` for 100 USDC).
131 pub margin: f64,
132 /// Leverage multiplier (e.g. `10.0` for 10×).
133 pub leverage: f64,
134 /// Slippage protection: max unspecified token amount. `0` = no limit.
135 pub unspecified_amount_limit: u128,
136}
137
138/// Client-facing parameters for opening a maker (LP) position.
139///
140/// The SDK converts these to contract types automatically:
141/// - `margin` → scaled to 6 decimals
142/// - `price_lower` / `price_upper` → converted to ticks
143#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
144pub struct OpenMakerParams {
145 /// Margin in USDC (e.g. `1000.0`).
146 pub margin: f64,
147 /// Lower bound of the price range.
148 pub price_lower: f64,
149 /// Upper bound of the price range.
150 pub price_upper: f64,
151 /// Liquidity amount to provide.
152 pub liquidity: u128,
153 /// Maximum amount of token0 willing to deposit.
154 pub max_amt0_in: u128,
155 /// Maximum amount of token1 willing to deposit.
156 pub max_amt1_in: u128,
157}
158
159/// Client-facing parameters for closing a position.
160#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
161pub struct CloseParams {
162 /// Minimum amount of token0 to receive (slippage protection).
163 pub min_amt0_out: u128,
164 /// Minimum amount of token1 to receive (slippage protection).
165 pub min_amt1_out: u128,
166 /// Maximum amount of token1 willing to pay.
167 pub max_amt1_in: u128,
168}
169
170/// Client-facing parameters for adjusting a position's notional exposure.
171#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
172pub struct AdjustNotionalParams {
173 /// USD delta: positive to increase notional, negative to decrease.
174 pub usd_delta: f64,
175 /// Maximum perp token amount for slippage protection. `u128::MAX` = no limit.
176 pub perp_limit: u128,
177}
178
179/// Client-facing parameters for adjusting a position's margin.
180#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
181pub struct AdjustMarginParams {
182 /// Margin delta in USDC: positive to deposit, negative to withdraw.
183 pub margin_delta: f64,
184}
185
186// ── Result types ────────────────────────────────────────────────────
187
188/// Result of opening a position (taker or maker).
189///
190/// Parsed from the `PositionOpened` event in the transaction receipt.
191#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
192pub struct OpenResult {
193 /// Minted position NFT token ID.
194 pub pos_id: U256,
195 /// Whether this is a maker position.
196 pub is_maker: bool,
197 /// Perp token delta (signed). Positive = long, negative = short.
198 pub perp_delta: f64,
199 /// USD delta (signed).
200 pub usd_delta: f64,
201 /// Lower tick of the position's price range.
202 pub tick_lower: i32,
203 /// Upper tick of the position's price range.
204 pub tick_upper: i32,
205}
206
207/// Result of adjusting a position's notional size.
208///
209/// Parsed from the `NotionalAdjusted` event in the transaction receipt.
210#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
211pub struct AdjustNotionalResult {
212 /// New cumulative perp delta after adjustment (signed).
213 pub new_perp_delta: f64,
214 /// Perp delta from this specific swap (signed).
215 pub swap_perp_delta: f64,
216 /// USD delta from this specific swap (signed).
217 pub swap_usd_delta: f64,
218 /// Funding settled during this adjustment.
219 pub funding: f64,
220 /// Utilization fee charged.
221 pub utilization_fee: f64,
222 /// Auto-deleveraging amount.
223 pub adl: f64,
224 /// Trading fees charged.
225 pub trading_fees: f64,
226}
227
228/// Result of adjusting a position's margin.
229///
230/// Parsed from the `MarginAdjusted` event in the transaction receipt.
231#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
232pub struct AdjustMarginResult {
233 /// New margin after adjustment.
234 pub new_margin: f64,
235}
236
237/// Result of closing a position.
238///
239/// Parsed from the `PositionClosed` event in the transaction receipt.
240#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
241pub struct CloseResult {
242 /// Transaction hash.
243 pub tx_hash: B256,
244 /// Whether this was a maker position.
245 pub was_maker: bool,
246 /// Whether the position was liquidated.
247 pub was_liquidated: bool,
248 /// If the close was partial, the remaining position's NFT token ID.
249 /// `None` means the position was fully closed.
250 pub remaining_position_id: Option<U256>,
251 /// Perp delta at exit (signed).
252 pub exit_perp_delta: f64,
253 /// USD delta at exit (signed).
254 pub exit_usd_delta: f64,
255 /// Net USD delta after settlement.
256 pub net_usd_delta: f64,
257 /// Funding settled at close.
258 pub funding: f64,
259 /// Utilization fee charged.
260 pub utilization_fee: f64,
261 /// Auto-deleveraging amount.
262 pub adl: f64,
263 /// Liquidation fee (zero if not liquidated).
264 pub liquidation_fee: f64,
265 /// Net margin returned.
266 pub net_margin: f64,
267}
268
269/// Result of a swap simulation via `quoteSwap`.
270///
271/// All values are human-readable (USDC as f64, perp delta as f64).
272#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
273pub struct SwapQuote {
274 /// Perp token delta (positive = received, negative = spent).
275 pub perp_delta: f64,
276 /// USD delta (positive = received, negative = spent).
277 pub usd_delta: f64,
278}
279
280/// Result of simulating a taker position open via `quoteOpenTakerPosition`.
281///
282/// All values are human-readable.
283#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
284pub struct OpenTakerQuote {
285 /// Perp token delta (positive = long exposure, negative = short).
286 pub perp_delta: f64,
287 /// USD delta (positive = received, negative = spent).
288 pub usd_delta: f64,
289}
290
291/// Result of simulating a maker position open via `quoteOpenMakerPosition`.
292///
293/// All values are human-readable.
294#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
295pub struct OpenMakerQuote {
296 /// Perp token delta.
297 pub perp_delta: f64,
298 /// USD delta.
299 pub usd_delta: f64,
300}
301
302#[cfg(test)]
303mod tests {
304 use super::*;
305 use alloy::primitives::{B256, U256};
306
307 #[test]
308 fn open_result_serde_roundtrip() {
309 let result = OpenResult {
310 pos_id: U256::from(42),
311 is_maker: false,
312 perp_delta: -1234.567,
313 usd_delta: 98765.43,
314 tick_lower: -69090,
315 tick_upper: 69090,
316 };
317 let json = serde_json::to_string(&result).unwrap();
318 let recovered: OpenResult = serde_json::from_str(&json).unwrap();
319 assert_eq!(result, recovered);
320 }
321
322 #[test]
323 fn close_result_serde_roundtrip() {
324 let result = CloseResult {
325 tx_hash: B256::ZERO,
326 was_maker: false,
327 was_liquidated: false,
328 remaining_position_id: None,
329 exit_perp_delta: -100.0,
330 exit_usd_delta: 200.0,
331 net_usd_delta: 195.0,
332 funding: -0.5,
333 utilization_fee: 0.1,
334 adl: 0.0,
335 liquidation_fee: 0.0,
336 net_margin: 150.0,
337 };
338 let json = serde_json::to_string(&result).unwrap();
339 let recovered: CloseResult = serde_json::from_str(&json).unwrap();
340 assert_eq!(result, recovered);
341 }
342
343 #[test]
344 fn deployments_serde_roundtrip() {
345 let deployments = Deployments {
346 perp_manager: Address::ZERO,
347 usdc: Address::ZERO,
348 fees_module: None,
349 margin_ratios_module: None,
350 lockup_period_module: None,
351 sqrt_price_impact_limit_module: None,
352 };
353 let json = serde_json::to_string(&deployments).unwrap();
354 let recovered: Deployments = serde_json::from_str(&json).unwrap();
355 assert_eq!(deployments, recovered);
356 }
357}