tempo_node/rpc/dex/
mod.rs

1use crate::rpc::{TempoDexApiServer, dex::orders::OrdersResponse};
2use alloy_eips::{BlockId, BlockNumberOrTag};
3use alloy_primitives::{Address, B256, Sealable};
4use jsonrpsee::core::RpcResult;
5use reth_ethereum::evm::revm::database::StateProviderDatabase;
6use reth_evm::{EvmInternals, revm::database::CacheDB};
7use reth_node_api::{ConfigureEvm, NodePrimitives};
8use reth_provider::{BlockReaderIdExt, StateProviderFactory};
9use reth_rpc_eth_api::{RpcNodeCore, helpers::SpawnBlocking};
10use reth_rpc_eth_types::{EthApiError, error::FromEthApiError};
11use tempo_alloy::rpc::pagination::PaginationParams;
12use tempo_evm::TempoEvmConfig;
13use tempo_precompiles::{
14    stablecoin_exchange::{
15        Order as PrecompileOrder, Orderbook as PrecompileOrderbook, StablecoinExchange, TickLevel,
16        orderbook::{OrderbookHandler, compute_book_key},
17    },
18    storage::{ContractStorage, Handler, StorageCtx, evm::EvmPrecompileStorageProvider},
19};
20use tempo_primitives::TempoHeader;
21
22pub mod api;
23
24pub mod orders;
25pub use orders::{Order, OrdersFilters, Tick};
26
27mod books;
28pub use books::{Orderbook, OrderbooksFilter, OrderbooksResponse};
29
30mod error;
31pub use error::DexApiError;
32
33/// Default limit for pagination
34const DEFAULT_LIMIT: usize = 10;
35
36/// Maximum limit for pagination
37const MAX_LIMIT: usize = 100;
38
39/// The JSON-RPC handlers for the `dex_` namespace.
40#[derive(Debug, Clone, Default)]
41pub struct TempoDex<EthApi> {
42    eth_api: EthApi,
43}
44
45impl<EthApi> TempoDex<EthApi> {
46    /// Creates a new instance of the [`TempoDex`] wrapping the eth api instance.
47    pub const fn new(eth_api: EthApi) -> Self {
48        Self { eth_api }
49    }
50}
51
52impl<
53    EthApi: RpcNodeCore<Evm = TempoEvmConfig, Primitives: NodePrimitives<BlockHeader = TempoHeader>>,
54> TempoDex<EthApi>
55{
56    /// Implementation of the `dex_orders` implementation
57    fn orders(
58        &self,
59        params: PaginationParams<OrdersFilters>,
60    ) -> Result<OrdersResponse, DexApiError> {
61        let response = self.with_storage_at_block(BlockNumberOrTag::Latest.into(), || {
62            let exchange = StablecoinExchange::new();
63            let exchange_address = exchange.address();
64
65            // Determine which books to iterate based on filter
66            let base_token = params.filters.as_ref().and_then(|f| f.base_token);
67            let quote_token = params.filters.as_ref().and_then(|f| f.quote_token);
68            let book_keys = get_book_keys_for_iteration(&exchange, base_token, quote_token)?;
69
70            let is_bid = params
71                .filters
72                .as_ref()
73                .is_none_or(|f| f.is_bid.unwrap_or(false));
74
75            let cursor = params
76                .cursor
77                .map(|cursor| parse_order_cursor(&cursor))
78                .transpose()?;
79
80            let limit = params
81                .limit
82                .map(|l| l.min(MAX_LIMIT))
83                .unwrap_or(DEFAULT_LIMIT);
84
85            let mut all_orders: Vec<Order> = Vec::new();
86            let mut next_cursor = None;
87
88            // Iterate through books collecting orders until we reach the limit
89            for book_key in book_keys {
90                let orderbook = exchange.books(book_key)?;
91
92                // Check if this book matches the base/quote filter
93                if !orderbook.matches_tokens(base_token, quote_token) {
94                    continue;
95                }
96
97                let starting_order = if all_orders.is_empty() {
98                    cursor // Use cursor only for the first book
99                } else {
100                    None
101                };
102
103                let book_iterator = BookIterator::new(
104                    &orderbook,
105                    exchange_address,
106                    is_bid,
107                    starting_order,
108                    params.filters.clone(),
109                );
110
111                // Collect orders from this book, up to limit + 1
112                for order_result in book_iterator {
113                    let order = order_result?;
114                    let rpc_order = self.to_rpc_order(order, &orderbook);
115                    all_orders.push(rpc_order);
116
117                    // stop once we have limit + 1 orders, we can't always use the next order
118                    // ID as the next cursor because of queue and book boundaries
119                    if all_orders.len() > limit {
120                        // Use the last order for cursor
121                        let last = &all_orders[limit];
122                        next_cursor = Some(format!("0x{:x}", last.order_id));
123                        break;
124                    }
125                }
126
127                // If we have enough orders, stop iterating through books
128                if all_orders.len() > limit {
129                    break;
130                }
131            }
132
133            // Truncate to limit
134            all_orders.truncate(limit);
135            let orders = all_orders;
136
137            let response = OrdersResponse {
138                next_cursor,
139                orders,
140            };
141            Ok(response)
142        })?;
143        Ok(response)
144    }
145
146    /// Implementation of the `dex_orderbooks` endpoints
147    fn orderbooks(
148        &self,
149        params: PaginationParams<OrderbooksFilter>,
150    ) -> Result<OrderbooksResponse, DexApiError> {
151        // Get paginated orderbooks
152        let (items, next_cursor) = self.apply_pagination_to_orderbooks(params)?;
153
154        // Convert PrecompileOrderbooks to RPC Orderbooks
155        let orderbooks = items
156            .into_iter()
157            .map(|book| self.to_rpc_orderbook(&book))
158            .collect();
159
160        // Create response with next cursor
161        Ok(OrderbooksResponse {
162            next_cursor,
163            orderbooks,
164        })
165    }
166
167    /// Creates an `EvmPrecompileStorageProvider` at the given block.
168    /// This handles the boilerplate of creating the EVM context and state provider.
169    fn with_storage_at_block<F, R>(&self, at: BlockId, f: F) -> Result<R, DexApiError>
170    where
171        F: FnOnce() -> Result<R, DexApiError>,
172    {
173        // Get the header for the specified block
174        let provider = self.eth_api.provider();
175        let header = provider
176            .header_by_id(at)
177            .map_err(|e| DexApiError::Provider(Box::new(e)))?
178            .ok_or(DexApiError::HeaderNotFound(at))?;
179
180        let block_hash = header.hash_slow();
181        let state_provider = provider
182            .state_by_block_hash(block_hash)
183            .map_err(|e| DexApiError::Provider(Box::new(e)))?;
184
185        // Create EVM using state provider db
186        let db = CacheDB::new(StateProviderDatabase::new(state_provider));
187        let mut evm = self
188            .eth_api
189            .evm_config()
190            .evm_for_block(db, &header)
191            .map_err(|e| DexApiError::CreateEvm(Box::new(e)))?;
192
193        let ctx = evm.ctx_mut();
194        let internals = EvmInternals::new(&mut ctx.journaled_state, &ctx.block);
195        let mut storage = EvmPrecompileStorageProvider::new_max_gas(internals, &ctx.cfg);
196
197        StorageCtx::enter(&mut storage, f)
198    }
199
200    /// Creates a `StablecoinExchange` instance at the given block.
201    /// This builds on `with_storage_at_block` to provide the exchange.
202    fn with_exchange_at_block<F, R>(&self, at: BlockId, f: F) -> Result<R, DexApiError>
203    where
204        F: FnOnce(&mut StablecoinExchange) -> Result<R, DexApiError>,
205    {
206        self.with_storage_at_block(at, || {
207            let mut exchange = StablecoinExchange::new();
208            f(&mut exchange)
209        })
210    }
211
212    /// Applies pagination parameters (filtering, limiting) to orderbooks.
213    ///
214    /// Returns orderbooks and optional next cursor.
215    pub fn apply_pagination_to_orderbooks(
216        &self,
217        params: PaginationParams<OrderbooksFilter>,
218    ) -> Result<(Vec<PrecompileOrderbook>, Option<String>), DexApiError> {
219        self.with_exchange_at_block(BlockNumberOrTag::Latest.into(), |exchange| {
220            let base_token = params.filters.as_ref().and_then(|f| f.base_token);
221            let quote_token = params.filters.as_ref().and_then(|f| f.quote_token);
222            let keys = get_book_keys_for_iteration(exchange, base_token, quote_token)?;
223
224            // Find starting position based on cursor
225            let start_idx = if let Some(ref cursor_str) = params.cursor {
226                let cursor_key = parse_orderbook_cursor(cursor_str)?;
227
228                keys.iter()
229                    .position(|k| *k == cursor_key)
230                    .ok_or(DexApiError::OrderbookCursorNotFound(cursor_key))?
231            } else {
232                0
233            };
234
235            // Convert keys to orderbooks, starting from cursor position
236            let mut orderbooks = Vec::new();
237            let limit = params
238                .limit
239                .map(|l| l.min(MAX_LIMIT))
240                .unwrap_or(DEFAULT_LIMIT);
241
242            let mut iter = keys.into_iter().skip(start_idx);
243
244            // Take limit + 1 to check if there's a next page
245            for key in iter.by_ref() {
246                let book = exchange.books(key).map_err(DexApiError::Precompile)?;
247
248                // Apply filters if present
249                if let Some(ref filter) = params.filters
250                    && !orderbook_matches_filter(&book, filter)
251                {
252                    continue;
253                }
254
255                orderbooks.push(book);
256
257                // Stop if we have enough items
258                if orderbooks.len() >= limit {
259                    break;
260                }
261            }
262
263            let next_cursor = iter.next().map(|next_book| format!("0x{next_book}"));
264
265            Ok((orderbooks, next_cursor))
266        })
267    }
268
269    /// Converts a precompile order to a rpc order.
270    ///
271    /// Uses the orderbook to determine base and quote token.
272    fn to_rpc_order(&self, order: PrecompileOrder, book: &PrecompileOrderbook) -> Order {
273        let PrecompileOrder {
274            order_id,
275            maker,
276            book_key: _,
277            is_bid,
278            tick,
279            amount,
280            remaining,
281            prev,
282            next,
283            is_flip,
284            flip_tick,
285        } = order;
286
287        Order {
288            amount,
289            base_token: book.base,
290            flip_tick,
291            is_bid,
292            is_flip,
293            maker,
294            next,
295            order_id,
296            quote_token: book.quote,
297            prev,
298            remaining,
299            tick,
300        }
301    }
302
303    /// Converts a precompile orderbook to RPC orderbook format.
304    ///
305    /// ## Cursor Field
306    /// The `book_key` field in the returned Orderbook serves as the cursor
307    /// for pagination when requesting subsequent pages.
308    fn to_rpc_orderbook(&self, book: &PrecompileOrderbook) -> Orderbook {
309        let book_key = compute_book_key(book.base, book.quote);
310        let spread = if book.best_ask_tick != i16::MAX && book.best_bid_tick != i16::MIN {
311            book.best_ask_tick - book.best_bid_tick
312        } else {
313            0
314        };
315
316        Orderbook {
317            base_token: book.base,
318            quote_token: book.quote,
319            book_key,
320            best_ask_tick: book.best_ask_tick,
321            best_bid_tick: book.best_bid_tick,
322            spread,
323        }
324    }
325
326    /// Returns the orderbooks that should be filtered based on the filter params.
327    pub fn pick_orderbooks(
328        &self,
329        filter: OrderbooksFilter,
330    ) -> Result<Vec<PrecompileOrderbook>, DexApiError> {
331        // If both base and quote are specified, get just that specific orderbook
332        if let (Some(base), Some(quote)) = (filter.base_token, filter.quote_token) {
333            return Ok(vec![self.get_orderbook(base, quote)?]);
334        }
335
336        // Get all orderbooks and filter them
337        let all_books = self.get_all_books()?;
338
339        Ok(all_books
340            .into_iter()
341            .filter(|book| orderbook_matches_filter(book, &filter))
342            .collect())
343    }
344
345    /// Returns all orderbooks.
346    pub fn get_all_books(&self) -> Result<Vec<PrecompileOrderbook>, DexApiError> {
347        self.with_exchange_at_block(BlockNumberOrTag::Latest.into(), |exchange| {
348            let mut books = Vec::new();
349            for book_key in exchange.get_book_keys()? {
350                let book = exchange.books(book_key)?;
351                books.push(book);
352            }
353            Ok(books)
354        })
355    }
356
357    /// Returns an orderbook based on the base and quote tokens.
358    ///
359    /// ## Note
360    /// Single orderbook fetches don't require cursor pagination.
361    /// This is used when filters specify both base and quote tokens.
362    pub fn get_orderbook(
363        &self,
364        base: Address,
365        quote: Address,
366    ) -> Result<PrecompileOrderbook, DexApiError> {
367        self.with_exchange_at_block(BlockNumberOrTag::Latest.into(), |exchange| {
368            let book_key = compute_book_key(base, quote);
369            exchange.books(book_key).map_err(DexApiError::Precompile)
370        })
371    }
372}
373
374#[async_trait::async_trait]
375impl<
376    EthApi: RpcNodeCore<Evm = TempoEvmConfig, Primitives: NodePrimitives<BlockHeader = TempoHeader>>
377        + SpawnBlocking,
378> TempoDexApiServer for TempoDex<EthApi>
379{
380    /// Returns orders based on pagination parameters.
381    ///
382    /// ## Cursor
383    /// The cursor for this method is the **Order ID** (u128).
384    /// - When provided in the request, returns orders starting after the given order ID
385    /// - Returns `next_cursor` in the response containing the last order ID for the next page
386    async fn orders(&self, params: PaginationParams<OrdersFilters>) -> RpcResult<OrdersResponse> {
387        let this = self.clone();
388        self.eth_api
389            .spawn_blocking_io(move |_| {
390                Self::orders(&this, params)
391                    .map_err(EthApiError::from)
392                    .map_err(EthApi::Error::from_eth_err)
393            })
394            .await
395            .map_err(Into::into)
396    }
397
398    /// Returns orderbooks based on pagination parameters.
399    ///
400    /// ## Cursor
401    /// The cursor for this method is the **Book Key** (B256).
402    /// - When provided in the request, returns orderbooks starting after the given book key
403    /// - Returns `next_cursor` in the response containing the last book key for the next page
404    async fn orderbooks(
405        &self,
406        params: PaginationParams<OrderbooksFilter>,
407    ) -> RpcResult<OrderbooksResponse> {
408        let this = self.clone();
409        self.eth_api
410            .spawn_blocking_io(move |_| {
411                Self::orderbooks(&this, params)
412                    .map_err(EthApiError::from)
413                    .map_err(EthApi::Error::from_eth_err)
414            })
415            .await
416            .map_err(Into::into)
417    }
418}
419
420/// An iterator over orders for a specific orderbook
421pub struct BookIterator<'b> {
422    /// Optional filter to apply to orders
423    filter: Option<OrdersFilters>,
424    /// Whether or not to iterate over bids or asks.
425    bids: bool,
426    /// Address of the exchange
427    exchange_address: Address,
428    /// Starting order ID
429    starting_order: Option<u128>,
430    /// Current order ID
431    order: Option<u128>,
432    /// Orderbook information
433    orderbook: &'b PrecompileOrderbook,
434    /// Orderbook handler
435    handler: OrderbookHandler,
436    /// Inner precompile storage
437    storage: StorageCtx,
438}
439
440impl<'b> ContractStorage for BookIterator<'b> {
441    fn address(&self) -> Address {
442        self.exchange_address
443    }
444    fn storage(&mut self) -> &mut StorageCtx {
445        &mut self.storage
446    }
447}
448
449impl<'b> BookIterator<'b> {
450    /// Create a new book iterator, optionally with the given order ID as the starting order.
451    fn new(
452        orderbook: &'b PrecompileOrderbook,
453        exchange_address: Address,
454        bids: bool,
455        starting_order: Option<u128>,
456        filter: Option<OrdersFilters>,
457    ) -> Self {
458        let book_key = compute_book_key(orderbook.base, orderbook.quote);
459        Self {
460            filter,
461            bids,
462            exchange_address,
463            order: None,
464            starting_order,
465            orderbook,
466            handler: StablecoinExchange::new().books.at(book_key),
467            storage: StorageCtx::default(),
468        }
469    }
470
471    /// Try to get the next order, returning None when iteration is complete.
472    /// This is an alternative to using the Iterator trait that makes error handling more explicit.
473    pub fn try_next(&mut self) -> Result<Option<PrecompileOrder>, DexApiError> {
474        match self.next() {
475            None => Ok(None),
476            Some(Ok(order)) => Ok(Some(order)),
477            Some(Err(e)) => Err(e),
478        }
479    }
480
481    /// Get a PrecompileOrder from an order ID
482    pub fn get_order(&self, order_id: u128) -> Result<PrecompileOrder, DexApiError> {
483        StablecoinExchange::new()
484            .get_order(order_id)
485            .map_err(DexApiError::Precompile)
486    }
487
488    /// Get a TickLevel from a tick
489    pub fn get_price_level(&self, tick: i16) -> Result<TickLevel, DexApiError> {
490        self.handler
491            .get_tick_level_handler(tick, self.bids)
492            .read()
493            .map_err(DexApiError::Precompile)
494    }
495
496    /// Get the next initialized tick after the given tick
497    /// Returns None if there are no more ticks
498    pub fn get_next_tick(&mut self, tick: i16) -> Option<i16> {
499        let (next_tick, more_ticks) = self.handler.next_initialized_tick(tick, self.bids);
500
501        if more_ticks { Some(next_tick) } else { None }
502    }
503
504    /// Find the next order in the orderbook, starting from current position.
505    /// Returns the order ID of the next order, or None if no more orders.
506    fn find_next_order(&mut self) -> Result<Option<u128>, DexApiError> {
507        // If we have a starting order, use that to initialize
508        if let Some(starting_order) = self.starting_order.take() {
509            return Ok(Some(starting_order));
510        }
511
512        // If there is no current order we get the first one based on the best bid or ask tick
513        let Some(current_id) = self.order else {
514            let tick = if self.bids {
515                self.orderbook.best_bid_tick
516            } else {
517                self.orderbook.best_ask_tick
518            };
519
520            let price_level = self.get_price_level(tick)?;
521
522            // if the best bid level is empty then there are no more bids and we should stop the
523            // iteration
524            if price_level.is_empty() {
525                return Ok(None);
526            }
527
528            return Ok(Some(price_level.head));
529        };
530
531        let current_order = self.get_order(current_id)?;
532
533        // Now get the order after this one.
534        if current_order.next() != 0 {
535            Ok(Some(current_order.next()))
536        } else {
537            let tick = current_order.tick();
538
539            // find the next tick
540            let Some(next_tick) = self.get_next_tick(tick) else {
541                return Ok(None);
542            };
543
544            // get the price level for this tick so we can get the head of the price level
545            let price_level = self.get_price_level(next_tick)?;
546            if price_level.is_empty() {
547                return Ok(None);
548            }
549
550            // return the head of the price level as the next order
551            Ok(Some(price_level.head))
552        }
553    }
554}
555
556impl<'b> Iterator for BookIterator<'b> {
557    type Item = Result<PrecompileOrder, DexApiError>;
558
559    fn next(&mut self) -> Option<Self::Item> {
560        // keep searching until we find an order that matches the filter
561        loop {
562            let order_id = match self.find_next_order() {
563                Ok(Some(id)) => id,
564                Ok(None) => return None,
565                Err(e) => return Some(Err(e)),
566            };
567
568            let order = match self.get_order(order_id) {
569                Ok(o) => o,
570                Err(e) => return Some(Err(e)),
571            };
572
573            // update current position
574            self.order = Some(order_id);
575
576            // check if order passes filter
577            if let Some(ref filter) = self.filter {
578                if order_matches_filter(&order, filter) {
579                    return Some(Ok(order));
580                }
581            } else {
582                // no filter, return the order
583                return Some(Ok(order));
584            }
585        }
586    }
587}
588
589/// Checks if an orderbook matches the given filters
590fn orderbook_matches_filter(book: &PrecompileOrderbook, filter: &OrderbooksFilter) -> bool {
591    // Check base and quote token filters
592    if !book.matches_tokens(filter.base_token, filter.quote_token) {
593        return false;
594    }
595
596    // Check best ask tick range
597    if let Some(ref ask_range) = filter.best_ask_tick {
598        // Only filter if the book has a valid ask (not i16::MAX)
599        if book.best_ask_tick != i16::MAX && !ask_range.in_range(book.best_ask_tick) {
600            return false;
601        }
602    }
603
604    // Check best bid tick range
605    if let Some(ref bid_range) = filter.best_bid_tick {
606        // Only filter if the book has a valid bid (not i16::MIN)
607        if book.best_bid_tick != i16::MIN && !bid_range.in_range(book.best_bid_tick) {
608            return false;
609        }
610    }
611
612    // Check spread range
613    if let Some(ref spread_range) = filter.spread {
614        // Calculate spread only if both ticks are valid
615        if book.best_ask_tick != i16::MAX && book.best_bid_tick != i16::MIN {
616            let spread = book.best_ask_tick - book.best_bid_tick;
617            if !spread_range.in_range(spread) {
618                return false;
619            }
620        }
621    }
622
623    true
624}
625
626/// Checks if an order matches the given filters
627fn order_matches_filter(order: &PrecompileOrder, filter: &OrdersFilters) -> bool {
628    // Note: base_token and quote_token filtering is handled at the book level,
629    // not at the individual order level
630
631    // Check bid/ask side filter
632    if filter.is_bid.is_some_and(|is_bid| is_bid != order.is_bid) {
633        return false;
634    }
635
636    // Check flip filter
637    if filter
638        .is_flip
639        .is_some_and(|is_flip| is_flip != order.is_flip)
640    {
641        return false;
642    }
643
644    // Check maker filter
645    if filter.maker.is_some_and(|maker| maker != order.maker) {
646        return false;
647    }
648
649    // Check remaining amount range
650    if filter
651        .remaining
652        .as_ref()
653        .is_some_and(|remaining_range| !remaining_range.in_range(order.remaining))
654    {
655        return false;
656    }
657
658    // Check tick range
659    if filter
660        .tick
661        .as_ref()
662        .is_some_and(|tick_range| !tick_range.in_range(order.tick))
663    {
664        return false;
665    }
666
667    true
668}
669
670/// Parses a QUANTITY cursor string into a u128 for orders
671fn parse_order_cursor(cursor: &str) -> Result<u128, DexApiError> {
672    if let Some(hex_val) = cursor.strip_prefix("0x") {
673        u128::from_str_radix(hex_val, 16).map_err(Into::into)
674    } else {
675        Err(DexApiError::InvalidOrderCursor(cursor.to_string()))
676    }
677}
678
679/// Parses a cursor string into a B256 for orderbooks
680fn parse_orderbook_cursor(cursor: &str) -> Result<B256, DexApiError> {
681    cursor
682        .parse::<B256>()
683        .map_err(|_| DexApiError::InvalidOrderbookCursor(cursor.to_string()))
684}
685
686/// Gets book keys to iterate over. If both base and quote are specified, returns only that book.
687/// Otherwise returns all book keys (filtering happens later during iteration).
688fn get_book_keys_for_iteration(
689    exchange: &StablecoinExchange,
690    base_token: Option<Address>,
691    quote_token: Option<Address>,
692) -> Result<Vec<B256>, DexApiError> {
693    match (base_token, quote_token) {
694        (Some(base), Some(quote)) => Ok(vec![compute_book_key(base, quote)]),
695        _ => exchange.get_book_keys().map_err(DexApiError::Precompile),
696    }
697}