Skip to main content

algo_sdk/
pool.rs

1//! DEX pool books, AMM state, and pool metadata.
2
3use crate::L2Book;
4
5// =============================================================================
6// Per-Pool Books (A6 — separate from NBBO hot path)
7// =============================================================================
8
9/// Maximum number of individual pool slots tracked per symbol.
10pub const MAX_POOLS: usize = 32;
11
12/// WASM memory offset where PoolBooks is written by the runtime.
13/// Located after VenueBooks to avoid overlap.
14pub const POOL_BOOKS_WASM_OFFSET: u32 = 0x14000;
15
16/// Per-pool metadata: compact identity + fee data for a single DEX pool.
17///
18/// Fixed-size (48 bytes). Fits 32 pools in ~1.5KB within the PoolBooks region.
19/// Layout: address(32) + pair_index(2) + fee_bps(2) + venue_id(1) + protocol_id(1) +
20///         gas_cost_1e5(2) + gas_units(2) + gas_price_gwei(2) + native_price_cents(4) = 48.
21#[derive(Clone, Copy)]
22#[repr(C)]
23pub struct PoolMeta {
24    /// Pool contract address: 32 bytes (EVM uses first 20, Solana uses all 32).
25    pub address: [u8; 32],
26    /// Pair index for multi-asset pools (0 for standard 2-token pools).
27    pub pair_index: u16,
28    /// Pool swap fee in basis points (AMM fee, not gas).
29    pub fee_bps: u16,
30    /// Venue ID (VENUE_DEX_ARB, VENUE_DEX_SOL, etc.)
31    pub venue_id: u8,
32    /// Protocol: 0=unknown, 1=uniswap_v2, 2=uniswap_v3, 3=curve,
33    /// 4=balancer_v2, 5=aerodrome, 6=velodrome, 7=camelot,
34    /// 8=raydium_clmm, 9=orca_whirlpool
35    pub protocol_id: u8,
36    // ── Pre-computed gas cost (convenience) ──
37    /// Estimated total execution cost, 1 unit = $0.00001 (max $0.65).
38    /// Pre-computed: gas_units_1k × gas_price_gwei × native_price / 1e9 × 1e5.
39    /// Use `gas_cost_usd()` for f64 conversion. 0 = unknown.
40    pub gas_cost_1e5: u16,
41    // ── Raw gas inputs (for custom models) ──
42    /// Protocol gas units in thousands (e.g., 180 = 180,000 gas). 0 = unknown.
43    /// From edge's protocol.estimate_gas(). Use `gas_units()` for full value.
44    pub gas_units_1k: u16,
45    /// Current gas price on this chain in gwei (base + priority). 0 = unknown.
46    pub gas_price_gwei: u16,
47    /// Native token price in deci-dollars (1 unit = $0.10). ETH=$3500 → 35000. Max $6,553.50.
48    /// For exact native price, use `ChainFeeTable.fee_for_chain().native_price_1e9`.
49    pub native_price_deci_usd: u16,
50    pub _pad: [u8; 2],
51}
52
53impl Default for PoolMeta {
54    fn default() -> Self {
55        Self {
56            address: [0; 32],
57            pair_index: 0,
58            fee_bps: 0,
59            venue_id: 0,
60            protocol_id: 0,
61            gas_cost_1e5: 0,
62            gas_units_1k: 0,
63            gas_price_gwei: 0,
64            native_price_deci_usd: 0,
65            _pad: [0; 2],
66        }
67    }
68}
69
70impl PoolMeta {
71    /// Pre-computed gas execution cost in USD (f64). 0.0 if unknown.
72    #[inline(always)]
73    pub fn gas_cost_usd(&self) -> f64 {
74        self.gas_cost_1e5 as f64 / 100_000.0
75    }
76
77    /// Raw gas units (e.g., 180,000 for Uniswap V3). 0 if unknown.
78    #[inline(always)]
79    pub fn gas_units(&self) -> u64 {
80        self.gas_units_1k as u64 * 1000
81    }
82
83    /// Native token price in USD (e.g., 3500.0 for ETH). 0.0 if unknown.
84    #[inline(always)]
85    pub fn native_price_usd(&self) -> f64 {
86        self.native_price_deci_usd as f64 / 10.0
87    }
88
89    /// Compute gas cost from raw inputs (for custom models).
90    /// `gas_units × gas_price_gwei × native_price_usd / 1e9`
91    #[inline]
92    pub fn compute_gas_cost_usd(&self) -> f64 {
93        self.gas_units() as f64 * self.gas_price_gwei as f64 * self.native_price_usd() / 1e9
94    }
95
96    /// Total cost to execute on this pool: swap fee + gas, in bps of a given notional.
97    #[inline]
98    pub fn total_cost_bps(&self, notional_1e9: u64) -> f64 {
99        let fee = self.fee_bps as f64;
100        let gas_bps = if notional_1e9 > 0 {
101            (self.gas_cost_usd() * 10_000.0 * 1_000_000_000.0) / notional_1e9 as f64
102        } else {
103            0.0
104        };
105        fee + gas_bps
106    }
107}
108
109/// Per-pool depth books delivered to multi-venue algos.
110///
111/// Written to WASM memory at `POOL_BOOKS_WASM_OFFSET` (0x14000) on a
112/// separate cadence from NBBO — typically per-block, not per-tick.
113/// Algos read this on demand during `on_nbbo()` via fixed WASM offset.
114///
115/// ~23 KB total. Conditional memcpy: skipped when `pool_ct == 0`.
116#[derive(Clone)]
117#[repr(C)]
118pub struct PoolBooks {
119    /// Number of valid pool slots (0..MAX_POOLS).
120    pub pool_ct: u8,
121    pub _pad: [u8; 7],
122    /// Metadata for each pool slot.
123    pub metas: [PoolMeta; MAX_POOLS],
124    /// L2 books for each pool. `books[i]` corresponds to `metas[i]`.
125    pub books: [L2Book; MAX_POOLS],
126}
127
128impl Default for PoolBooks {
129    fn default() -> Self {
130        Self {
131            pool_ct: 0,
132            _pad: [0; 7],
133            metas: [PoolMeta::default(); MAX_POOLS],
134            books: [L2Book::default(); MAX_POOLS],
135        }
136    }
137}
138
139impl PoolBooks {
140    /// Look up the L2Book for a specific pool by address and pair_index.
141    #[inline]
142    pub fn book_for_pool(&self, addr: &[u8; 32], pair_index: u16) -> Option<&L2Book> {
143        let ct = self.pool_ct as usize;
144        for i in 0..ct {
145            if self.metas[i].address == *addr && self.metas[i].pair_index == pair_index {
146                return Some(&self.books[i]);
147            }
148        }
149        None
150    }
151
152    /// Direct access to the L2Book at a given slot index.
153    #[inline(always)]
154    pub fn book_at_slot(&self, slot: usize) -> &L2Book {
155        if slot < self.pool_ct as usize {
156            &self.books[slot]
157        } else {
158            &self.books[0]
159        }
160    }
161
162    /// Direct access to the PoolMeta at a given slot index.
163    #[inline(always)]
164    pub fn meta_at_slot(&self, slot: usize) -> &PoolMeta {
165        if slot < self.pool_ct as usize {
166            &self.metas[slot]
167        } else {
168            &self.metas[0]
169        }
170    }
171}
172
173// =============================================================================
174// POOL STATE TABLE (raw AMM state for strategy-side pricing)
175// =============================================================================
176
177/// WASM offset for PoolStateTable — after ExecutionPlanBuffer.
178/// ExecutionPlanBuffer at 0x22200 (2120 bytes) ends at ~0x22A48. Safe gap to 0x23000.
179pub const POOL_STATE_TABLE_WASM_OFFSET: u32 = 0x23000;
180
181/// Maximum pool states (1:1 correspondence with PoolBooks).
182pub const MAX_POOL_STATES: usize = 32;
183
184/// Pool type discriminant for PoolAmm.
185pub mod pool_type {
186    /// Constant-product AMM (Uniswap V2, Raydium V4/CPMM).
187    pub const V2: u8 = 0;
188    /// Concentrated liquidity (Uniswap V3, Raydium CLMM, Orca Whirlpool).
189    pub const V3_CLMM: u8 = 1;
190    /// Stableswap (Curve, multi-asset).
191    pub const STABLESWAP: u8 = 2;
192}
193
194/// Raw AMM state for a single pool. Slot `i` corresponds to `PoolBooks.metas[i]`.
195///
196/// Strategies use this to compute swap output from the pricing function directly
197/// rather than relying on pre-computed synthetic L2Books.
198///
199/// - **V2 pools**: use `reserve0()`, `reserve1()`, `fee_ppm`
200/// - **V3/CLMM pools**: use `sqrt_price_x64`, `tick`, `liquidity`, `fee_ppm`, `tick_spacing`
201#[derive(Clone, Copy, Debug)]
202#[repr(C)]
203pub struct PoolAmm {
204    // ── V2 reserves (split u128 for WASM portability) ──
205    /// Lower 64 bits of reserve0.
206    pub reserve0_lo: u64,
207    /// Upper 64 bits of reserve0 (usually 0 for practical pools).
208    pub reserve0_hi: u64,
209    /// Lower 64 bits of reserve1.
210    pub reserve1_lo: u64,
211    /// Upper 64 bits of reserve1.
212    pub reserve1_hi: u64,
213
214    // ── V3/CLMM state ──
215    /// sqrt(price) in Q64.64 fixed-point (converted from Q64.96 by CC).
216    /// To get price: `(sqrt_price_x64 as f64 / 2^64)^2`.
217    pub sqrt_price_x64: u128,    // offset 32, 16-byte aligned ✓
218    /// Active liquidity at the current tick.
219    pub liquidity: u128,          // offset 48, 16-byte aligned ✓
220
221    /// Current tick index. Price at tick `t` ≈ 1.0001^t.
222    pub tick: i32,
223    /// Tick spacing (V3 only: 1, 10, 60, 200).
224    pub tick_spacing: i32,
225
226    // ── Metadata ──
227    /// Swap fee in parts-per-million (3000 = 0.3%).
228    pub fee_ppm: u32,
229    /// Pool type: 0=V2, 1=V3/CLMM, 2=stableswap.
230    pub pool_type: u8,
231    pub _pad0: [u8; 3],
232    /// Block/slot number when this state was observed.
233    pub last_block: u64,
234    /// Nanosecond timestamp of last update.
235    pub last_update_ns: u64,
236}
237
238impl Default for PoolAmm {
239    fn default() -> Self {
240        // SAFETY: all-zero is valid for every field
241        unsafe { core::mem::zeroed() }
242    }
243}
244
245impl PoolAmm {
246    /// Reconstruct reserve0 as u128.
247    #[inline(always)]
248    pub fn reserve0(&self) -> u128 {
249        (self.reserve0_hi as u128) << 64 | self.reserve0_lo as u128
250    }
251
252    /// Reconstruct reserve1 as u128.
253    #[inline(always)]
254    pub fn reserve1(&self) -> u128 {
255        (self.reserve1_hi as u128) << 64 | self.reserve1_lo as u128
256    }
257
258    /// Whether this is a V2 (constant-product) pool.
259    #[inline(always)]
260    pub fn is_v2(&self) -> bool {
261        self.pool_type == pool_type::V2
262    }
263
264    /// Whether this is a V3/CLMM (concentrated liquidity) pool.
265    #[inline(always)]
266    pub fn is_v3(&self) -> bool {
267        self.pool_type == pool_type::V3_CLMM
268    }
269
270    /// Whether this state has been populated (non-zero block).
271    #[inline(always)]
272    pub fn is_valid(&self) -> bool {
273        self.last_block > 0
274    }
275
276    /// Fee as basis points (e.g. 3000 ppm → 30 bps).
277    #[inline(always)]
278    pub fn fee_bps(&self) -> u32 {
279        self.fee_ppm / 100
280    }
281}
282
283/// Table of raw AMM states, parallel to PoolBooks.
284///
285/// Written to WASM memory at `POOL_STATE_TABLE_WASM_OFFSET` (0x23000).
286/// Conditional write: skipped when `count == 0`.
287#[derive(Clone, Copy)]
288#[repr(C)]
289pub struct PoolStateTable {
290    /// Number of valid entries (matches `PoolBooks.pool_ct`).
291    pub count: u8,
292    /// Padding to align `states` at 16-byte boundary (u128 fields in PoolAmm).
293    pub _pad: [u8; 15],
294    /// Per-pool AMM states. `states[i]` corresponds to `PoolBooks.metas[i]`.
295    pub states: [PoolAmm; MAX_POOL_STATES],
296}
297
298impl Default for PoolStateTable {
299    fn default() -> Self {
300        Self {
301            count: 0,
302            _pad: [0; 15],
303            states: [PoolAmm::default(); MAX_POOL_STATES],
304        }
305    }
306}
307
308impl PoolStateTable {
309    /// Get the AMM state for a pool slot.
310    #[inline(always)]
311    pub fn state_at(&self, slot: usize) -> &PoolAmm {
312        if slot < self.count as usize {
313            &self.states[slot]
314        } else {
315            &self.states[0]
316        }
317    }
318}
319
320// Compile-time ABI checks for PoolBooks
321const _: () = assert!(
322    POOL_BOOKS_WASM_OFFSET as usize >= crate::VENUE_BOOKS_WASM_OFFSET as usize,
323    "POOL_BOOKS_WASM_OFFSET must not overlap VenueBooks"
324);
325
326// Compile-time ABI checks for PoolStateTable
327const _: () = assert!(
328    core::mem::size_of::<PoolAmm>() == 96,
329    "PoolAmm must be exactly 96 bytes"
330);
331const _: () = assert!(
332    core::mem::size_of::<PoolStateTable>() == 3088,
333    "PoolStateTable must be exactly 3088 bytes (16 + 32*96)"
334);
335const _: () = assert!(
336    POOL_STATE_TABLE_WASM_OFFSET as usize + core::mem::size_of::<PoolStateTable>() < 0x1000000,
337    "PoolStateTable exceeds WASM 16MB memory limit"
338);