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);