Skip to main content

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}