From 060fcbae8153c0d8999df00c5242625912b1e20b Mon Sep 17 00:00:00 2001 From: randalllionelkharkrang Date: Sat, 11 Oct 2025 13:07:43 +0530 Subject: [PATCH 01/15] Added order types Orderbook execution --- Cargo.lock | 14 ++++++ Cargo.toml | 2 + pallets/orderbook/Cargo.toml | 46 ++++++++++++++++++++ pallets/orderbook/src/benchmarking.rs | 0 pallets/orderbook/src/lib.rs | 29 +++++++++++++ pallets/orderbook/src/mock.rs | 0 pallets/orderbook/src/tests.rs | 0 pallets/orderbook/src/types.rs | 61 +++++++++++++++++++++++++++ pallets/template/src/lib.rs | 8 +++- 9 files changed, 158 insertions(+), 2 deletions(-) create mode 100644 pallets/orderbook/Cargo.toml create mode 100644 pallets/orderbook/src/benchmarking.rs create mode 100644 pallets/orderbook/src/lib.rs create mode 100644 pallets/orderbook/src/mock.rs create mode 100644 pallets/orderbook/src/tests.rs create mode 100644 pallets/orderbook/src/types.rs diff --git a/Cargo.lock b/Cargo.lock index 6670cfe..39dff84 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5919,6 +5919,20 @@ dependencies = [ "sp-staking", ] +[[package]] +name = "pallet-orderbook" +version = "0.1.0" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "parity-scale-codec", + "scale-info", + "sp-core", + "sp-io", + "sp-runtime", +] + [[package]] name = "pallet-session" version = "40.0.0" diff --git a/Cargo.toml b/Cargo.toml index ca52370..8e6efc4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ members = [ "node", "pallets/template", "pallets/assets", + "pallets/orderbook", "runtime", ] resolver = "2" @@ -62,6 +63,7 @@ pallet-aura = { version = "39.0.0", default-features = false } pallet-assets = { version = "0.1.0", path = "./pallets/assets", default-features = false } pallet-balances = { version = "41.1.0", default-features = false } pallet-grandpa = { version = "40.0.0", default-features = false } +pallet-orderbook = {version = "0.1.0", default-features = false} pallet-sudo = { version = "40.0.0", default-features = false } pallet-timestamp = { version = "39.0.0", default-features = false } pallet-transaction-payment-rpc-runtime-api = { version = "40.0.0", default-features = false } diff --git a/pallets/orderbook/Cargo.toml b/pallets/orderbook/Cargo.toml new file mode 100644 index 0000000..369d7b7 --- /dev/null +++ b/pallets/orderbook/Cargo.toml @@ -0,0 +1,46 @@ +[package] +name = "pallet-orderbook" +description = "FRAME pallet for defining Orderbook storage maps, with matching engine " +version = "0.1.0" +license = "Unlicense" +authors.workspace = true +homepage.workspace = true +repository.workspace = true +edition.workspace = true +publish = false + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +codec = { features = ["derive"], workspace = true } +frame-benchmarking = { optional = true, workspace = true } +frame-support.workspace = true +frame-system.workspace = true +scale-info = { features = ["derive"], workspace = true } + +[dev-dependencies] +sp-core = { default-features = true, workspace = true } +sp-io = { default-features = true, workspace = true } +sp-runtime = { default-features = true, workspace = true } + +[features] +default = ["std"] +std = [ + "codec/std", + "frame-benchmarking?/std", + "frame-support/std", + "frame-system/std", + "scale-info/std", +] +runtime-benchmarks = [ + "frame-benchmarking/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", +] +try-runtime = [ + "frame-support/try-runtime", + "frame-system/try-runtime", + "sp-runtime/try-runtime", +] diff --git a/pallets/orderbook/src/benchmarking.rs b/pallets/orderbook/src/benchmarking.rs new file mode 100644 index 0000000..e69de29 diff --git a/pallets/orderbook/src/lib.rs b/pallets/orderbook/src/lib.rs new file mode 100644 index 0000000..332e565 --- /dev/null +++ b/pallets/orderbook/src/lib.rs @@ -0,0 +1,29 @@ + +#![cfg_attr(not(feature = "std"), no_std)] + +mod types; +pub use pallet::*; + +#[cfg(test)] +mod mock; + +#[cfg(test)] +mod tests; + +#[cfg(feature="runtime-benchmarks")] +mod benchmarking; + +#[frame_support::pallet] +pub mod pallet { + use super::*; + use frame_support::pallet_prelude::*; + use frame_system::pallet_prelude::*; + + #[pallet::pallet] + pub struct Pallet(_); + + #[frame_support::config] + + //#[frame_support::event] + //#[frame_support::error] +} \ No newline at end of file diff --git a/pallets/orderbook/src/mock.rs b/pallets/orderbook/src/mock.rs new file mode 100644 index 0000000..e69de29 diff --git a/pallets/orderbook/src/tests.rs b/pallets/orderbook/src/tests.rs new file mode 100644 index 0000000..e69de29 diff --git a/pallets/orderbook/src/types.rs b/pallets/orderbook/src/types.rs new file mode 100644 index 0000000..e09893e --- /dev/null +++ b/pallets/orderbook/src/types.rs @@ -0,0 +1,61 @@ +use codec::{Decode, Encode, MaxEncodedLen}; +use scale_info::TypeInfo; +use sp_runtime::RuntimeDebug; + +#[derive(Encode,Decode, Clone,Copy,RuntimeDebug, PartialEq, Eq, TypeInfo, MaxEncodedLen)] +pub enum OrderSide { + Buy, + Sell, +} +#[derive(Encode,Decode, Clone ,Copy,RuntimeDebug, PartialEq, Eq, TypeInfo, MaxEncodedLen)] +pub enum OrderStatus { + Filled, + PartiallyFilled, + Cancelled, + Expired, + Open, +} + +#[derive(Encode,Decode, Clone, Debug,Copy,RuntimeDebug, PartialEq, Eq, TypeInfo,MaxEncodedLen)] +pub enum OrderType{ + Market, + Limit, + // will add the other stuff like IOK, Stop etc later +} + +#[derive(Encode, Decode, Clone, Copy, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)] + +pub struct MarketPair{ + pub base_asset : AssetId, // btc/usdt pair + pub quote_asset : AssetId, +} + +#[derive(Encode,Decode, Clone, Debug,RuntimeDebug, PartialEq, Eq, TypeInfo,MaxEncodedLen)] +#[scale_info(skip_type_params(T))] +pub struct Order{ + pub order_id: OrderId, + pub trader: T::AccountId, + pub side: OrderSide, + pub status: OrderStatus, + pub order_type: OrderType, + pub price: Amount, + pub quantity: Amount, + pub filled_quantity: Amount +} + +#[derive(Encode,Decode, Clone, Debug,RuntimeDebug, PartialEq, Eq, TypeInfo,MaxEncodedLen)] +#[scale_info(skip_type_params(T))] +pub struct Trade { + pub trade_id: TradeId, + pub buyer: T::AccountId, + pub seller: T::AccountId, + pub buy_order_id: OrderId, + pub sell_order_id: OrderId, + pub price: Amount, + pub quantity: Amount, +} + +pub type OrderId = u64; +pub type TradeId = u64; +pub type AssetId = u32; +pub type Balance = u128; \ No newline at end of file diff --git a/pallets/template/src/lib.rs b/pallets/template/src/lib.rs index 3b629cf..90dfe37 100644 --- a/pallets/template/src/lib.rs +++ b/pallets/template/src/lib.rs @@ -81,7 +81,6 @@ pub mod pallet { #[pallet::config] pub trait Config: frame_system::Config { /// The overarching runtime event type. - #[allow(deprecated)] type RuntimeEvent: From> + IsType<::RuntimeEvent>; /// A type representing the weights required by the dispatchables of this pallet. type WeightInfo: WeightInfo; @@ -195,4 +194,9 @@ pub mod pallet { let new = old.checked_add(1).ok_or(Error::::StorageOverflow)?; // Update the value in storage with the incremented result. Something::::put(new); - Ok(() \ No newline at end of file + Ok(()) + }, + } + } + } +} From 855ca247b2f954dbfcf894c307f1dc193629188c Mon Sep 17 00:00:00 2001 From: randalllionelkharkrang Date: Sat, 11 Oct 2025 16:50:50 +0530 Subject: [PATCH 02/15] Added template with OBStorage, BlockOrders and events/errors/exstrinsics/hooks --- pallets/orderbook/Cargo.toml | 1 + pallets/orderbook/src/benchmarking.rs | 1 + pallets/orderbook/src/lib.rs | 222 +++++++++++++++++++++++++- pallets/orderbook/src/mock.rs | 1 + pallets/orderbook/src/tests.rs | 1 + pallets/orderbook/src/types.rs | 17 +- 6 files changed, 231 insertions(+), 12 deletions(-) diff --git a/pallets/orderbook/Cargo.toml b/pallets/orderbook/Cargo.toml index 369d7b7..4dc6d77 100644 --- a/pallets/orderbook/Cargo.toml +++ b/pallets/orderbook/Cargo.toml @@ -18,6 +18,7 @@ frame-benchmarking = { optional = true, workspace = true } frame-support.workspace = true frame-system.workspace = true scale-info = { features = ["derive"], workspace = true } +sp-core.workspace = true [dev-dependencies] sp-core = { default-features = true, workspace = true } diff --git a/pallets/orderbook/src/benchmarking.rs b/pallets/orderbook/src/benchmarking.rs index e69de29..354fde5 100644 --- a/pallets/orderbook/src/benchmarking.rs +++ b/pallets/orderbook/src/benchmarking.rs @@ -0,0 +1 @@ +// to be impl \ No newline at end of file diff --git a/pallets/orderbook/src/lib.rs b/pallets/orderbook/src/lib.rs index 332e565..bf7b787 100644 --- a/pallets/orderbook/src/lib.rs +++ b/pallets/orderbook/src/lib.rs @@ -3,6 +3,7 @@ mod types; pub use pallet::*; +use crate::types::*; #[cfg(test)] mod mock; @@ -16,14 +17,225 @@ mod benchmarking; #[frame_support::pallet] pub mod pallet { use super::*; - use frame_support::pallet_prelude::*; - use frame_system::pallet_prelude::*; + use frame_support::{Blake2_128Concat, pallet_prelude:: *}; + use frame_system::pallet_prelude::{OriginFor, *}; + use sp_core::Get; + //use sp_runtime::legacy::byte_sized_error::DispatchError; #[pallet::pallet] pub struct Pallet(_); - #[frame_support::config] + #[pallet::config] + pub trait Config: frame_system::Config { + type RuntimeEvent: From> + IsType<::RuntimeEvent>; - //#[frame_support::event] - //#[frame_support::error] + // maximum orders at any price level not sure if needed. this will be on the blockOrders cache + #[pallet::constant] + type MaxPendingOrders: Get; + + //keeping this to prevent DDoS attacks for cancelling too many orders + #[pallet::constant] + type MaxCancellationOrders: Get; + + #[pallet::constant] + type MaxOrders: Get; + + #[pallet::constant] + type MaxUserOrders: Get; + + } + + // =========================== + // Persisten storage + // =========================== + #[pallet::storage] + pub type Bids = StorageMap< + _, + Blake2_128Concat, + Amount, + BoundedVec, + ValueQuery, + >; + + #[pallet::storage] + pub type Asks = StorageMap< + _, + Blake2_128Concat, + Amount, + BoundedVec, + ValueQuery, + >; + + // =========================== + // Cache + // =========================== + + #[pallet::storage] + pub type PendingOrders = StorageMap< + _, + Blake2_128Concat, + Amount, + BoundedVec, + ValueQuery + + >; + + #[pallet::storage] + pub type PendingBids = StorageMap< + _, + Blake2_128Concat, + Amount, + BoundedVec, + ValueQuery + + >; + + //Keeping this so that users can easily access their orders + #[pallet::storage] + pub type UserOrders = StorageMap< + _, + Blake2_128Concat, + T::AccountId, + BoundedVec, + ValueQuery + >; + + #[pallet::storage] + pub type NextOrderId = StorageValue<_, OrderId, ValueQuery>; + + #[pallet::storage] + pub type NextTradeId = StorageValue<_, TradeId, ValueQuery>; + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + OrderPlaced{ + order_id: OrderId, + side: OrderSide, + price: Amount, + quantity: Amount + }, + TradeExecuted{ + trade_id: TradeId, + buy_order_id: OrderId, + sell_order_id: OrderId, + buyer: T::AccountId, + seller: T::AccountId, + price: Amount, + quantity: Amount, + + }, + OrderCancelled { + order_id: OrderId, + trader: T::AccountId, + }, + OrderFilled { + order_id: OrderId, + trader: T::AccountId, + }, + OrderPartiallyFilled { + order_id: OrderId, + trader: T::AccountId, + filled_quantity: Amount, + remaining_quantity: Amount, + }, + // we are putting this event, so that we know its requested but it could not be processed perhaps + CancellationRequested { + order_id: OrderId, + trader: T::AccountId, + }, + MatchingCompleted { + total_trades: u32, + total_volume: Amount, + } + } + + #[pallet::error] + pub enum Error { + /// Order not found + OrderNotFound, + + /// Not the order owner + NotOrderOwner, + + /// Order not active + OrderNotActive, + + /// Price must be > 0 + InvalidPrice, + + /// Quantity must be > 0 + InvalidQuantity, + + /// Insufficient balance + InsufficientBalance, + + /// Too many pending orders this block + TooManyPendingOrders, + + /// Too many pending cancellations this block + TooManyPendingCancellations, + + /// Arithmetic overflow + ArithmeticOverflow, + + /// Arithmetic underflow + ArithmeticUnderflow, + + /// Failed to unreserve funds + FailedToUnreserveFunds, + + /// No matching orders + NoMatchingOrders, + } + + + // ======================================== + // HOOKS FOR MATCHING + // ======================================== + + #[pallet::hooks] + impl Hooks> for Pallet { + fn on_finalize(_n: BlockNumberFor) { + todo!("Actual matching") + // Then we need to match stuff from block order + + // if not found look at the OBStorage + + // Clear the cache + } + } + + + // ============================================================ + // EXTRINSICS + // ============================================================ + + #[pallet::call] + impl Pallet { + /// Place a limit order + #[pallet::call_index(0)] + #[pallet::weight(10000)] + pub fn place_order( + origin: OriginFor, + side: OrderSide, + price: Amount, + quantity: Amount, + ordertype: OrderType + ) -> DispatchResult { + //let trader = ensure_signed(origin)?; + todo!("We need to process this to place orders") + } + + #[pallet::call_index(1)] + #[pallet::weight(10000)] + pub fn cancel_order( + origin: OriginFor, + orderid: OrderId, + ) -> DispatchResult { + //let trader = ensure_signed(origin)?; + + todo!("We need to process this to cancel orders ") + } + } } \ No newline at end of file diff --git a/pallets/orderbook/src/mock.rs b/pallets/orderbook/src/mock.rs index e69de29..a4fba45 100644 --- a/pallets/orderbook/src/mock.rs +++ b/pallets/orderbook/src/mock.rs @@ -0,0 +1 @@ +//to be impl \ No newline at end of file diff --git a/pallets/orderbook/src/tests.rs b/pallets/orderbook/src/tests.rs index e69de29..a4fba45 100644 --- a/pallets/orderbook/src/tests.rs +++ b/pallets/orderbook/src/tests.rs @@ -0,0 +1 @@ +//to be impl \ No newline at end of file diff --git a/pallets/orderbook/src/types.rs b/pallets/orderbook/src/types.rs index e09893e..ca60845 100644 --- a/pallets/orderbook/src/types.rs +++ b/pallets/orderbook/src/types.rs @@ -1,8 +1,10 @@ use codec::{Decode, Encode, MaxEncodedLen}; use scale_info::TypeInfo; -use sp_runtime::RuntimeDebug; +use frame_support::sp_runtime::RuntimeDebug; +use frame_system::*; +use frame_support::pallet_prelude::*; -#[derive(Encode,Decode, Clone,Copy,RuntimeDebug, PartialEq, Eq, TypeInfo, MaxEncodedLen)] +#[derive(Encode,Decode, Clone,Copy,RuntimeDebug, PartialEq, Eq, TypeInfo, MaxEncodedLen,DecodeWithMemTracking)] pub enum OrderSide { Buy, Sell, @@ -16,7 +18,7 @@ pub enum OrderStatus { Open, } -#[derive(Encode,Decode, Clone, Debug,Copy,RuntimeDebug, PartialEq, Eq, TypeInfo,MaxEncodedLen)] +#[derive(Encode,Decode, Clone,Copy,RuntimeDebug, PartialEq, Eq, TypeInfo,MaxEncodedLen,DecodeWithMemTracking)] pub enum OrderType{ Market, Limit, @@ -30,7 +32,7 @@ pub struct MarketPair{ pub quote_asset : AssetId, } -#[derive(Encode,Decode, Clone, Debug,RuntimeDebug, PartialEq, Eq, TypeInfo,MaxEncodedLen)] +#[derive(Encode,Decode, Clone,RuntimeDebug, PartialEq, Eq, TypeInfo,MaxEncodedLen)] #[scale_info(skip_type_params(T))] pub struct Order{ pub order_id: OrderId, @@ -40,10 +42,11 @@ pub struct Order{ pub order_type: OrderType, pub price: Amount, pub quantity: Amount, - pub filled_quantity: Amount + pub filled_quantity: Amount, + pub ttl: Option, } -#[derive(Encode,Decode, Clone, Debug,RuntimeDebug, PartialEq, Eq, TypeInfo,MaxEncodedLen)] +#[derive(Encode,Decode, Clone,RuntimeDebug, PartialEq, Eq, TypeInfo,MaxEncodedLen)] #[scale_info(skip_type_params(T))] pub struct Trade { pub trade_id: TradeId, @@ -58,4 +61,4 @@ pub struct Trade { pub type OrderId = u64; pub type TradeId = u64; pub type AssetId = u32; -pub type Balance = u128; \ No newline at end of file +pub type Amount = u128; \ No newline at end of file From 4956a60e5babcbd1cdcc8c24a5be59797ea9fde4 Mon Sep 17 00:00:00 2001 From: randalllionelkharkrang Date: Mon, 13 Oct 2025 17:33:41 +0530 Subject: [PATCH 03/15] Added full orderbook dex functionality --- Cargo.lock | 2 + pallets/assets/src/lib.rs | 2 +- pallets/orderbook/Cargo.toml | 3 + pallets/orderbook/src/engine.rs | 374 ++++++++++++++++++++++++++++++++ pallets/orderbook/src/lib.rs | 348 ++++++++++++++++++++++++++--- 5 files changed, 702 insertions(+), 27 deletions(-) create mode 100644 pallets/orderbook/src/engine.rs diff --git a/Cargo.lock b/Cargo.lock index 39dff84..5ee73a8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5926,11 +5926,13 @@ dependencies = [ "frame-benchmarking", "frame-support", "frame-system", + "pallet-assets", "parity-scale-codec", "scale-info", "sp-core", "sp-io", "sp-runtime", + "sp-std", ] [[package]] diff --git a/pallets/assets/src/lib.rs b/pallets/assets/src/lib.rs index 9c67d08..e8fdb41 100644 --- a/pallets/assets/src/lib.rs +++ b/pallets/assets/src/lib.rs @@ -195,7 +195,7 @@ pub mod pallet { })?; //move it to transferred .ie to account - LockedBalance::::mutate(to, asset_id, |balance| { + FreeBalance::::mutate(to, asset_id, |balance| { *balance = balance.saturating_add(amount) }); diff --git a/pallets/orderbook/Cargo.toml b/pallets/orderbook/Cargo.toml index 4dc6d77..c716fb4 100644 --- a/pallets/orderbook/Cargo.toml +++ b/pallets/orderbook/Cargo.toml @@ -15,10 +15,13 @@ targets = ["x86_64-unknown-linux-gnu"] [dependencies] codec = { features = ["derive"], workspace = true } frame-benchmarking = { optional = true, workspace = true } +pallet-assets = { path = "../assets", default-features = false } frame-support.workspace = true frame-system.workspace = true scale-info = { features = ["derive"], workspace = true } sp-core.workspace = true +sp-runtime.workspace = true +sp-std = "14.0.0" [dev-dependencies] sp-core = { default-features = true, workspace = true } diff --git a/pallets/orderbook/src/engine.rs b/pallets/orderbook/src/engine.rs new file mode 100644 index 0000000..05f3937 --- /dev/null +++ b/pallets/orderbook/src/engine.rs @@ -0,0 +1,374 @@ +use codec::{Decode, Encode}; +use frame_support::{ + ensure, + pallet_prelude::*, +}; +use sp_runtime::traits::Zero; +use sp_std::{collections::btree_map::BTreeMap, vec::Vec}; + +// Import our types +use crate::types::*; +use frame_system::Config; + + + +// This will match with the cache structure +pub fn match_pending_internal( + pending_bids: BTreeMap>, + pending_asks: BTreeMap>, + orders_map: &mut BTreeMap>, +) -> Result<(Vec>, Vec), DispatchError> { + + let mut bid_book = pending_bids; + let mut ask_book = pending_asks; + let mut trades = Vec::new(); + + let mut all_pending_ids = Vec::new(); + + for (_price, order_ids) in bid_book.iter(){ + all_pending_ids.extend(order_ids.clone()); + } + + for (_price, order_ids) in ask_book.iter() { + all_pending_ids.extend(order_ids.clone()); + } + + all_pending_ids.sort(); + + for order_id in all_pending_ids { + let mut order = match orders_map.get(&order_id) { + Some(o) => o.clone(), + None => continue, + }; + + remove_from_orderbook(order_id, &order, &mut bid_book, &mut ask_book); + + let order_trades = match order.side{ + OrderSide::Buy => match_buy_order(&mut order, &mut ask_book, orders_map)?, + OrderSide::Sell => match_sell_order(&mut order, &mut bid_book, orders_map)?, + }; + + trades.extend(order_trades); + + + + if order.status != OrderStatus::Filled { + add_order_to_book(&order, &mut bid_book, &mut ask_book); + orders_map.insert(order_id, order.clone()); + } else { + orders_map.remove(&order.order_id); + } + } + + let mut unmatched = Vec::new(); + for (_price, ids) in bid_book.iter() { + unmatched.extend(ids.clone()); + } + for (_price, ids) in ask_book.iter() { + unmatched.extend(ids.clone()); + } + + Ok((trades, unmatched)) +} + +pub fn match_persistent_storage( + persistent_bids: &mut BTreeMap>, + persistent_asks: &mut BTreeMap>, + unmatched: Vec, + orders_map: &mut BTreeMap>, +) -> Result>, DispatchError>{ + + let mut trades = Vec::new(); + + for order_id in unmatched.iter(){ + let mut order = match orders_map.get(&order_id) { + Some(o) => o.clone(), + None => continue, + }; + + let order_trades = match order.side { + OrderSide::Buy => match_buy_order(&mut order, persistent_asks, orders_map), + OrderSide::Sell => match_sell_order(&mut order, persistent_bids, orders_map) + }; + + trades.extend(order_trades.unwrap()); + + if order.status == OrderStatus::Filled { + orders_map.remove(&order_id); // Remove filled orders + } else { + orders_map.insert(*order_id, order.clone()); // Keep active orders + // also add to persistent storage + add_order_to_book(&order, persistent_bids, persistent_asks); + } + } + Ok(trades) +} + +fn remove_from_orderbook( + order_id: OrderId, + order: &Order, + bid_book: &mut BTreeMap>, + ask_book: &mut BTreeMap>, +) { + let book = match order.side { + OrderSide::Buy => bid_book, + OrderSide::Sell => ask_book, + }; + + if let Some(ids) = book.get_mut(&order.price) { + ids.retain(|id| *id != order_id); + if ids.is_empty() { + book.remove(&order.price); + } + } +} + +fn add_order_to_book( + order: &Order, + bid_book: &mut BTreeMap>, + ask_book: &mut BTreeMap> +){ + let book = match order.side { + OrderSide::Buy => bid_book, + OrderSide::Sell => ask_book, + }; + + book.entry(order.price).or_insert_with(Vec::new).push(order.order_id); +} + +fn match_buy_order( + buy_order: &mut Order, + ask_book: &mut BTreeMap>, + orders_map: &mut BTreeMap>, +) -> Result>, DispatchError> { + + let mut trades = Vec::new(); + let mut prices_to_remove = Vec::new(); + + // Get all ask prices sorted (lowest first) + let ask_prices: Vec = ask_book.keys().cloned().collect(); + + for price in ask_prices.iter() { + + // Check if we can match at this price + match buy_order.order_type { + OrderType::Market => { + // Market orders match at any price + }, + OrderType::Limit => { + if buy_order.price < *price { + break; // Too expensive, stop + } + }, + } + + // Check if buy order still needs filling + if remaining_quantity(buy_order) == 0 { + break; + } + + // Get sell orders at this price level + if let Some(sell_order_ids) = ask_book.get_mut(price) { + + let mut indices_to_remove = Vec::new(); + + // Match with each sell order (FIFO - price-time priority) + for (idx, sell_order_id) in sell_order_ids.iter().enumerate() { + + // Get the sell order + let mut sell_order = match orders_map.get(sell_order_id) { + Some(o) => o.clone(), + None => continue, + }; + + // Execute trade at this price level (maker's price) + let trade = execute_trade(buy_order, &mut sell_order, *price)?; + trades.push(trade); + + // Update sell order in orders_map + orders_map.insert(*sell_order_id, sell_order.clone()); + + // If sell order is filled, mark for removal + if sell_order.status == OrderStatus::Filled { + indices_to_remove.push(idx); + } + + // If buy order is filled, stop matching + if buy_order.status == OrderStatus::Filled { + break; + } + } + + // Remove filled orders (reverse to maintain indices) + for idx in indices_to_remove.iter().rev() { + sell_order_ids.remove(*idx); + } + + // If no orders left at this price, mark for removal + if sell_order_ids.is_empty() { + prices_to_remove.push(*price); + } + } + } + + // Clean up empty price levels + for price in prices_to_remove { + ask_book.remove(&price); + } + + Ok(trades) +} + + +fn match_sell_order( + sell_order: &mut Order, + bid_book: &mut BTreeMap>, + orders_map: &mut BTreeMap>, +) -> Result>, DispatchError> { + + let mut trades = Vec::new(); + let mut prices_to_remove = Vec::new(); + + // Get all bid prices sorted (highest first) + let mut bid_prices: Vec = bid_book.keys().cloned().collect(); + bid_prices.sort_by(|a, b| b.cmp(a)); // Reverse sort + + for price in bid_prices.iter() { + + // Check if we can match at this price + match sell_order.order_type { + OrderType::Market => { + // Market orders match at any price + }, + OrderType::Limit => { + if sell_order.price > *price { + break; // Too cheap, stop + } + }, + } + + // Check if sell order still needs filling + if remaining_quantity(sell_order) == 0 { + break; + } + + // Get buy orders at this price level + if let Some(buy_order_ids) = bid_book.get_mut(price) { + + let mut indices_to_remove = Vec::new(); + + // Match with each buy order (FIFO - price-time priority) + for (idx, buy_order_id) in buy_order_ids.iter().enumerate() { + + // Get the buy order + let mut buy_order = match orders_map.get(buy_order_id) { + Some(o) => o.clone(), + None => continue, + }; + + // Execute trade at this price level (maker's price) + let trade = execute_trade(&mut buy_order, sell_order, *price)?; + trades.push(trade); + + // Update buy order in orders_map + orders_map.insert(*buy_order_id, buy_order.clone()); + + // If buy order is filled, mark for removal + if buy_order.status == OrderStatus::Filled { + indices_to_remove.push(idx); + } + + // If sell order is filled, stop matching + if sell_order.status == OrderStatus::Filled { + break; + } + } + + // Remove filled orders (reverse to maintain indices) + for idx in indices_to_remove.iter().rev() { + buy_order_ids.remove(*idx); + } + + // If no orders left at this price, mark for removal + if buy_order_ids.is_empty() { + prices_to_remove.push(*price); + } + } + } + + // Clean up empty price levels + for price in prices_to_remove { + bid_book.remove(&price); + } + + Ok(trades) +} + + +fn remaining_quantity( + order: &mut Order +) -> Amount { + order.quantity.saturating_sub(order.filled_quantity) +} + +fn execute_trade( + buy_order: &mut Order, + sell_order: &mut Order, + match_price: Amount, +) -> Result, DispatchError> { + + let buy_remaining = remaining_quantity(buy_order); + let sell_remaining = remaining_quantity(sell_order); + let trade_qty = buy_remaining.min(sell_remaining); + + // update buy order + buy_order.filled_quantity = buy_order.filled_quantity.checked_add(trade_qty).ok_or("ArithmeticOverFlow")?; + + if buy_order.filled_quantity == buy_order.quantity { + buy_order.status = OrderStatus::Filled; + } else { + buy_order.status = OrderStatus::PartiallyFilled; + } + + //update sell order + sell_order.filled_quantity = sell_order.filled_quantity.checked_add(trade_qty).ok_or("ArithmeticOverFlow")?; + + if sell_order.filled_quantity == sell_order.quantity { + sell_order.status = OrderStatus::Filled; + } else { + sell_order.status = OrderStatus::PartiallyFilled; + } + + //Everything updated, now to emit the trades + Ok(Trade { + trade_id:0, // placeholder + buyer: buy_order.trader.clone(), + seller: sell_order.trader.clone(), + buy_order_id: buy_order.order_id, + sell_order_id: sell_order.order_id, + price: match_price, + quantity: trade_qty, + }) +} + +// now for cancellation +pub fn process_cancellations( + order_ids: Vec, + bid_book: &mut BTreeMap>, + ask_book: &mut BTreeMap>, + orders_map: &mut BTreeMap> +) -> Result<(), DispatchError> { + + for order_id in order_ids { + if let Some(mut order) = orders_map.get(&order_id).cloned() { + + order.status = OrderStatus::Cancelled; + + remove_from_orderbook(order_id, &order, bid_book, ask_book); + + orders_map.insert(order_id, order.clone()); + + } + } + Ok(()) +} \ No newline at end of file diff --git a/pallets/orderbook/src/lib.rs b/pallets/orderbook/src/lib.rs index bf7b787..2d809cc 100644 --- a/pallets/orderbook/src/lib.rs +++ b/pallets/orderbook/src/lib.rs @@ -1,9 +1,11 @@ #![cfg_attr(not(feature = "std"), no_std)] - -mod types; +#![allow(ambiguous_glob_reexports)] +pub mod types; +mod engine; pub use pallet::*; -use crate::types::*; +//pub use crate::types; +//pub use pallet_assets::*; #[cfg(test)] mod mock; @@ -14,19 +16,30 @@ mod tests; #[cfg(feature="runtime-benchmarks")] mod benchmarking; + #[frame_support::pallet] pub mod pallet { - use super::*; + //use std::intrinsics::saturating_add; + + use core::u32; + + //use super::*; + use crate::{engine::*, types::{Amount, Order, OrderId, OrderSide, OrderStatus, OrderType, Trade, TradeId}}; + use frame_support::{Blake2_128Concat, pallet_prelude:: *}; use frame_system::pallet_prelude::{OriginFor, *}; use sp_core::Get; + use pallet_assets as assets; + use sp_std::{collections::btree_map::BTreeMap, vec::Vec}; + use assets::{USDT,ETH}; + //use assets::*; //use sp_runtime::legacy::byte_sized_error::DispatchError; #[pallet::pallet] pub struct Pallet(_); #[pallet::config] - pub trait Config: frame_system::Config { + pub trait Config: frame_system::Config + pallet_assets::Config { type RuntimeEvent: From> + IsType<::RuntimeEvent>; // maximum orders at any price level not sure if needed. this will be on the blockOrders cache @@ -48,6 +61,14 @@ pub mod pallet { // =========================== // Persisten storage // =========================== + + // not sure if this needed yet, so just keeping it + #[pallet::storage] + pub type Orders = StorageMap<_, Blake2_128Concat, OrderId, Order, OptionQuery>; + + #[pallet::storage] + pub type Trades = StorageMap<_, Blake2_128Concat, TradeId, Trade, OptionQuery>; + #[pallet::storage] pub type Bids = StorageMap< _, @@ -71,25 +92,27 @@ pub mod pallet { // =========================== #[pallet::storage] - pub type PendingOrders = StorageMap< + pub type PendingAsks = StorageMap< _, Blake2_128Concat, Amount, - BoundedVec, - ValueQuery - + BoundedVec, + ValueQuery, >; #[pallet::storage] pub type PendingBids = StorageMap< - _, - Blake2_128Concat, - Amount, - BoundedVec, - ValueQuery - - >; + _, + Blake2_128Concat, + Amount, + BoundedVec, + ValueQuery, + >; + + #[pallet::storage] + pub type PendingCancellations = StorageValue<_, BoundedVec, ValueQuery>; + //Keeping this so that users can easily access their orders #[pallet::storage] pub type UserOrders = StorageMap< @@ -172,6 +195,9 @@ pub mod pallet { /// Too many pending orders this block TooManyPendingOrders, + + //too many user orders + TooManyUserOrders, /// Too many pending cancellations this block TooManyPendingCancellations, @@ -197,12 +223,222 @@ pub mod pallet { #[pallet::hooks] impl Hooks> for Pallet { fn on_finalize(_n: BlockNumberFor) { - todo!("Actual matching") - // Then we need to match stuff from block order + let mut orders_map = BTreeMap::new(); + + //load all orders, will need to modify for sure + for (order_id, order) in Orders::::iter() { + orders_map.insert(order_id, order); + } + + //================================ + // These will load the temp caches + //================================ - // if not found look at the OBStorage - // Clear the cache + // Load pending bids + let mut pending_bids = BTreeMap::new(); + for (price, order_ids) in PendingBids::::iter() { + pending_bids.insert(price, order_ids.into_inner()); + } + + // Load pending asks + let mut pending_asks = BTreeMap::new(); + for (price, order_ids) in PendingAsks::::iter() { + pending_asks.insert(price, order_ids.into_inner()); + } + + // Load persistent bids + let mut persistent_bids = BTreeMap::new(); + for (price, order_ids) in Bids::::iter() { + persistent_bids.insert(price, order_ids.into_inner()); + } + + // Load persistent asks + let mut persistent_asks = BTreeMap::new(); + for (price, order_ids) in Asks::::iter() { + persistent_asks.insert(price, order_ids.into_inner()); + } + + let cancellations = PendingCancellations::::get(); + if !cancellations.is_empty() { + let _ = process_cancellations::( + cancellations.into_inner(), + &mut persistent_bids, + &mut persistent_asks, + &mut orders_map, + ); + } + + let mut all_trades: Vec> = Vec::new(); + + // here we are matching first only from the temp cache + let (pending_trades, unmatched) = match match_pending_internal(pending_bids, pending_asks, &mut orders_map) { + Ok(result) => result, + Err(_) => (Vec::new(), Vec::new()), + }; + + all_trades.extend(pending_trades); + + + if !unmatched.is_empty() { + let persistent_trades = match match_persistent_storage( + &mut persistent_bids, + &mut persistent_asks, + unmatched, + &mut orders_map, + ) { + Ok(trades) => trades, + Err(_) => Vec::new(), + }; + + all_trades.extend(persistent_trades); + } + + /// At this point, we have in memory done all necessary transactions + // Now we need to adjust order/money management + let mut total_volume = 0u128; + + for trade in all_trades.iter_mut() { + // Set trade_id + let trade_id = NextTradeId::::get(); + trade.trade_id = trade_id; + + // Transfer USDT from buyer to seller + let usdt_amount = trade.price.saturating_mul(trade.quantity); + let _ = assets::Pallet::::transfer_locked( + &trade.buyer, + &trade.seller, + USDT, + usdt_amount, + ); + + // Transfer ETH from seller to buyer + let _ = assets::Pallet::::transfer_locked( + &trade.seller, + &trade.buyer, + ETH, + trade.quantity, + ); + + // Unlock funds for both parties + let _ = assets::Pallet::::unlock_funds(&trade.seller, USDT, usdt_amount); + let _ = assets::Pallet::::unlock_funds(&trade.buyer, ETH, trade.quantity); + + // Store trade + Trades::::insert(trade_id, trade.clone()); + NextTradeId::::put(trade_id + 1); + + // Emit event + Self::deposit_event(Event::TradeExecuted { + trade_id, + buy_order_id: trade.buy_order_id, + sell_order_id: trade.sell_order_id, + buyer: trade.buyer.clone(), + seller: trade.seller.clone(), + price: trade.price, + quantity: trade.quantity, + }); + + total_volume = total_volume.saturating_add(usdt_amount); + } + + + // Now we need to unlock funds which are cancelled + for (order_id, order) in orders_map.iter() { + if order.status == OrderStatus::Cancelled { + let remaining = order.quantity.saturating_sub(order.filled_quantity); + + if remaining > 0 { + let (asset, amount) = match order.side { + OrderSide::Buy => { + let total = order.price.saturating_mul(remaining); + (USDT, total) + }, + OrderSide::Sell => (ETH, remaining), + }; + + let _ = assets::Pallet::::unlock_funds(&order.trader, asset, amount); + + Self::deposit_event(Event::OrderCancelled { + order_id: *order_id, + trader: order.trader.clone(), + }); + } + } + } + + // Emit events for filled/partially filled: + for (order_id, order) in orders_map.iter() { + Orders::::insert(order_id, order); + + // Emit events for filled/partially filled orders + if order.status == OrderStatus::Filled { + Self::deposit_event(Event::OrderFilled { + order_id: *order_id, + trader: order.trader.clone(), + }); + } else if order.status == OrderStatus::PartiallyFilled { + let remaining = order.quantity.saturating_sub(order.filled_quantity); + Self::deposit_event(Event::OrderPartiallyFilled { + order_id: *order_id, + trader: order.trader.clone(), + filled_quantity: order.filled_quantity, + remaining_quantity: remaining, + }); + } + } + + // Here we modify the StorageDoubleMap + for (price, order_ids) in persistent_bids.iter(){ + if !order_ids.is_empty() { + match BoundedVec::::try_from(order_ids.clone()) { + Ok(bounded) => { + Bids::::insert(price, bounded); + }, + Err(_) => { + // Doing this so that its save and bounded(altho this is mostly guaranteed because its from pending asks/bids and also pendingcancellations) + let truncated: Vec = order_ids.iter() + .take(T::MaxOrders::get() as usize) + .cloned() + .collect(); + + if let Ok(bounded) = BoundedVec::::try_from(truncated.clone()) { + Bids::::insert(price, bounded); + } + } + } + } + } + + for (price, order_ids) in persistent_asks.iter(){ + if !order_ids.is_empty() { + match BoundedVec::::try_from(order_ids.clone()) { + Ok(bounded) => { + Asks::::insert(price, bounded); + }, + Err(_) => { + // Doing this so that its save and bounded(altho this is mostly guaranteed because its from pending asks/bids and also pendingcancellations) + let truncated: Vec = order_ids.iter() + .take(T::MaxOrders::get() as usize) + .cloned() + .collect(); + + if let Ok(bounded) = BoundedVec::::try_from(truncated.clone()) { + Asks::::insert(price, bounded); + } + } + } + } + } + + // Clear Pending Bids and Asks + let _ = PendingBids::::clear(u32::MAX, None); + let _ = PendingAsks::::clear(u32::MAX, None); + + //EMIT event about complete trades + Self::deposit_event(Event::MatchingCompleted { total_trades:all_trades.len() as u32, total_volume: total_volume }); + + //Ok(()) } } @@ -221,21 +457,81 @@ pub mod pallet { side: OrderSide, price: Amount, quantity: Amount, - ordertype: OrderType + order_type: OrderType ) -> DispatchResult { - //let trader = ensure_signed(origin)?; - todo!("We need to process this to place orders") + let trader = ensure_signed(origin)?; + ensure!(price > 0, Error::::InvalidPrice); + ensure!(quantity > 0, Error::::InvalidQuantity); + + let (asset, amount_to_lock) = match side { + OrderSide::Buy => { + let total_amount = price.checked_mul(quantity).ok_or(Error::::ArithmeticOverflow)?; + (USDT,total_amount) + }, + OrderSide::Sell => { + (ETH, quantity) + } + }; + assets::Pallet::::lock_funds(&trader, asset, amount_to_lock)?; + + let order_id = NextOrderId::::get(); + let order = Order { + order_id, + trader: trader.clone(), + side, + status: OrderStatus::Open, + order_type, + price, + quantity, + filled_quantity: 0, + ttl: None, + }; + + Orders::::insert(order_id, order); + if side == OrderSide::Buy { + PendingBids::::try_mutate(price, |orders| { + orders.try_push(order_id).map_err(|_| Error::::TooManyPendingOrders) + })?; + } else { + PendingAsks::::try_mutate(price, |orders| { + orders.try_push(order_id).map_err(|_| Error::::TooManyPendingOrders) + })?; + } + + UserOrders::::try_mutate(trader.clone(), |orders| { + orders.try_push(order_id).map_err(|_| Error::::TooManyUserOrders) + })?; + + NextOrderId::::put(order_id + 1); + + Self::deposit_event(Event::OrderPlaced { order_id: order_id, side: side, price: price, quantity: quantity }); + + Ok(()) + } #[pallet::call_index(1)] #[pallet::weight(10000)] pub fn cancel_order( origin: OriginFor, - orderid: OrderId, + order_id: OrderId, ) -> DispatchResult { - //let trader = ensure_signed(origin)?; + let trader = ensure_signed(origin)?; + + let order = Orders::::get(order_id).ok_or(Error::::OrderNotFound)?; + + ensure!(trader == order.trader, Error::::NotOrderOwner); + ensure!( + order.status != OrderStatus::Filled, Error::::OrderNotActive + ); + + PendingCancellations::::try_mutate(|cancellations| { + cancellations.try_push(order_id).map_err(|_| Error::::TooManyPendingCancellations) + })?; + + Self::deposit_event(Event::CancellationRequested { order_id: order.order_id, trader: trader }); - todo!("We need to process this to cancel orders ") + Ok(()) } } } \ No newline at end of file From a554780524fb52a7037787541f1f8425096a29d5 Mon Sep 17 00:00:00 2001 From: randalllionelkharkrang Date: Mon, 13 Oct 2025 17:36:00 +0530 Subject: [PATCH 04/15] Added appropriate Readme.md --- README.md | 466 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 274 insertions(+), 192 deletions(-) diff --git a/README.md b/README.md index 7f36a99..b59d352 100644 --- a/README.md +++ b/README.md @@ -1,232 +1,314 @@ -# Substrate Node Template +# Substrate Orderbook DEX -A fresh [Substrate](https://substrate.io/) node, ready for hacking :rocket: +A decentralized exchange (DEX) built on Substrate with a limit orderbook and batch matching engine. -A standalone version of this template is available for each release of Polkadot -in the [Substrate Developer Hub Parachain -Template](https://github.com/substrate-developer-hub/substrate-node-template/) -repository. The parachain template is generated directly at each Polkadot -release branch from the [Solochain Template in -Substrate](https://github.com/paritytech/polkadot-sdk/tree/master/templates/solochain) -upstream +## Overview -It is usually best to use the stand-alone version to start a new project. All -bugs, suggestions, and feature requests should be made upstream in the -[Substrate](https://github.com/paritytech/polkadot-sdk/tree/master/substrate) -repository. +This project implements a fully functional orderbook-based DEX on Substrate, featuring: -## Getting Started +- **Limit & Market Orders**: Support for both order types with price-time priority matching +- **Batch Matching Engine**: Orders are matched once per block at finalization for optimal gas efficiency +- **Two-Phase Matching**: Pending orders match internally first, then with the persistent orderbook +- **Custom Assets Pallet**: Manages USDT and ETH balances with lock/unlock functionality +- **Atomic Settlement**: All trades are settled atomically with proper fund transfers -Depending on your operating system and Rust version, there might be additional -packages required to compile this template. Check the -[Install](https://docs.substrate.io/install/) instructions for your platform for -the most common dependencies. Alternatively, you can use one of the [alternative -installation](#alternatives-installations) options. +## Architecture -Fetch solochain template code: +### Two-Phase Design +1. **Order Submission** (during block): Orders validated and queued in temporary cache +2. **Batch Matching** (on_finalize): All orders matched once, funds settled, cache cleared -```sh -git clone https://github.com/paritytech/polkadot-sdk-solochain-template.git solochain-template +### Benefits +- โœ… Constant-time order submission (no matching during extrinsic) +- โœ… Single matching pass per block (more efficient than per-order matching) +- โœ… Better price discovery (orders within same block match first) +- โœ… Prevents race conditions and MEV attacks -cd solochain-template -``` +## Pallets -### Build +### 1. Assets Pallet (`pallets/assets`) -๐Ÿ”จ Use the following command to build the node without launching it: +Manages user balances for trading assets (USDT and ETH). + +**Key Features:** +- Deposit/withdraw funds +- Lock funds for active orders +- Unlock funds for cancellations +- Transfer locked funds for trade settlement + +**Storage:** +- `FreeBalance`: Available user balances +- `LockedBalance`: Funds locked in active orders + +### 2. Orderbook Pallet (`pallets/orderbook`) + +Core DEX functionality with orderbook matching engine. + +**Key Features:** +- Place limit and market orders +- Cancel pending orders +- Automatic batch matching at block finalization +- Price-time priority (FIFO within price levels) +- Partial order fills +- TTL-based order expiry + +**Storage:** +- **Persistent:** + - `Orders`: All order details + - `Trades`: Trade history + - `Bids`/`Asks`: Active orderbook (price โ†’ order IDs) + - `UserOrders`: User's order list + +- **Temporary Cache (cleared each block):** + - `PendingBids`/`PendingAsks`: Orders submitted this block + - `PendingCancellations`: Cancellation requests + +**Extrinsics:** +- `place_order(side, price, quantity, order_type)`: Submit a new order +- `cancel_order(order_id)`: Cancel an existing order + +### 3. Matching Engine (`pallets/orderbook/src/engine.rs`) + +Pure matching logic separated from pallet for clarity. + +**Functions:** +- `match_pending_internal`: Match pending orders amongst themselves +- `match_with_persistent`: Match unmatched orders with persistent orderbook +- `match_buy_order`/`match_sell_order`: Core matching logic with price-time priority +- `execute_trade`: Update order states and create trade records +- `process_cancellations`: Handle order cancellations + +## Matching Flow -```sh -cargo build --release +``` +Block N starts + โ†“ +Users submit orders via place_order() + โ†’ Orders stored in Orders + โ†’ Added to PendingBids/PendingAsks cache + โ†’ Funds locked + โ†“ +Block N ends โ†’ on_finalize() triggered + โ†“ +1. Process cancellations + โ†’ Remove from orderbook + โ†’ Unlock funds + โ†“ +2. Match pending orders internally + โ†’ Pending orders match with each other first + โ†’ Returns trades + unmatched orders + โ†“ +3. Match unmatched with persistent orderbook + โ†’ Try to match survivors with existing orders + โ†’ Add remainder to persistent orderbook + โ†“ +4. Execute all trades + โ†’ transfer_locked (buyer โ†’ seller: USDT) + โ†’ transfer_locked (seller โ†’ buyer: ETH) + โ†’ unlock_funds for both parties + โ†’ Store trade records + โ†’ Emit events + โ†“ +5. Update storage + โ†’ Save modified orders + โ†’ Save persistent orderbook + โ†’ Clear pending cache + โ†“ +Block N+1 starts fresh ``` -### Embedded Docs +## Example Trade -After you build the project, you can use the following command to explore its -parameters and subcommands: +``` +Initial State: + Alice: 10,000 USDT (free) + Bob: 100 ETH (free) + +Alice places order: + place_order(Buy, 100 USDT, 10 ETH, Limit) + โ†’ lock_funds(Alice, USDT, 1000) + โ†’ Add to PendingBids + +Bob places order: + place_order(Sell, 98 USDT, 10 ETH, Limit) + โ†’ lock_funds(Bob, ETH, 10) + โ†’ Add to PendingAsks + +on_finalize(): + 1. Match pending orders + โ†’ Bob's sell @ 98 matches Alice's buy @ 100 + โ†’ Execute at maker price: 100 USDT (Alice's limit) + + 2. Settle trade + โ†’ transfer_locked(Alice โ†’ Bob, USDT, 1000) + โ†’ transfer_locked(Bob โ†’ Alice, ETH, 10) + โ†’ unlock_funds(Bob, USDT, 1000) + โ†’ unlock_funds(Alice, ETH, 10) + +Final State: + Alice: 9,000 USDT, 10 ETH + Bob: 1,000 USDT, 90 ETH +``` -```sh -./target/release/solochain-template-node -h +## Types + +### Order +```rust +pub struct Order { + pub order_id: OrderId, + pub trader: T::AccountId, + pub side: OrderSide, // Buy or Sell + pub status: OrderStatus, // Open, PartiallyFilled, Filled, Cancelled, Expired + pub order_type: OrderType, // Market or Limit + pub price: Amount, // Price per unit + pub quantity: Amount, // Total quantity + pub filled_quantity: Amount, // Amount filled so far + pub ttl: Option, // Time-to-live (blocks) +} +``` + +### Trade +```rust +pub struct Trade { + pub trade_id: TradeId, + pub buyer: T::AccountId, + pub seller: T::AccountId, + pub buy_order_id: OrderId, + pub sell_order_id: OrderId, + pub price: Amount, + pub quantity: Amount, +} ``` -You can generate and view the [Rust -Docs](https://doc.rust-lang.org/cargo/commands/cargo-doc.html) for this template -with this command: +## Getting Started + +### Build ```sh -cargo +nightly doc --open +cargo build --release ``` -### Single-Node Development Chain - -The following command starts a single-node development chain that doesn't -persist state: +### Run Development Chain ```sh ./target/release/solochain-template-node --dev ``` -To purge the development chain's state, run the following command: +### Purge Chain State ```sh ./target/release/solochain-template-node purge-chain --dev ``` -To start the development chain with detailed logging, run the following command: +### Connect with Polkadot-JS Apps -```sh -RUST_BACKTRACE=1 ./target/release/solochain-template-node -ldebug --dev +Visit [Polkadot/Substrate Portal](https://polkadot.js.org/apps/#/explorer?rpc=ws://localhost:9944) and connect to your local node. + +## Usage + +### 1. Deposit Funds (Assets Pallet) + +```javascript +// Deposit 10,000 USDT +api.tx.assets.deposit(0, 10000000000); // USDT = asset_id 0 + +// Deposit 100 ETH +api.tx.assets.deposit(1, 100000000000); // ETH = asset_id 1 ``` -Development chains: +### 2. Place Order (Orderbook Pallet) + +```javascript +// Buy 10 ETH at 100 USDT each (limit order) +api.tx.orderbook.placeOrder( + { Buy }, // side + 100000000000, // price (100 USDT) + 10000000000, // quantity (10 ETH) + { Limit } // order_type +); + +// Sell 10 ETH at market price +api.tx.orderbook.placeOrder( + { Sell }, + 0, // price (ignored for market orders) + 10000000000, + { Market } +); +``` -- Maintain state in a `tmp` folder while the node is running. -- Use the **Alice** and **Bob** accounts as default validator authorities. -- Use the **Alice** account as the default `sudo` account. -- Are preconfigured with a genesis state (`/node/src/chain_spec.rs`) that - includes several pre-funded development accounts. +### 3. Cancel Order +```javascript +// Cancel order by ID +api.tx.orderbook.cancelOrder(123); +``` -To persist chain state between runs, specify a base path by running a command -similar to the following: +### 4. Query Orders -```sh -// Create a folder to use as the db base path -$ mkdir my-chain-state - -// Use of that folder to store the chain state -$ ./target/release/solochain-template-node --dev --base-path ./my-chain-state/ - -// Check the folder structure created inside the base path after running the chain -$ ls ./my-chain-state -chains -$ ls ./my-chain-state/chains/ -dev -$ ls ./my-chain-state/chains/dev -db keystore network +```javascript +// Get order details +const order = await api.query.orderbook.orders(orderId); + +// Get user's orders +const userOrders = await api.query.orderbook.userOrders(accountId); + +// Get bids at price level +const bids = await api.query.orderbook.bids(100000000000); + +// Get asks at price level +const asks = await api.query.orderbook.asks(100000000000); ``` -### Connect with Polkadot-JS Apps Front-End - -After you start the node template locally, you can interact with it using the -hosted version of the [Polkadot/Substrate -Portal](https://polkadot.js.org/apps/#/explorer?rpc=ws://localhost:9944) -front-end by connecting to the local node endpoint. A hosted version is also -available on [IPFS](https://dotapps.io/). You can -also find the source code and instructions for hosting your own instance in the -[`polkadot-js/apps`](https://github.com/polkadot-js/apps) repository. - -### Multi-Node Local Testnet - -If you want to see the multi-node consensus algorithm in action, see [Simulate a -network](https://docs.substrate.io/tutorials/build-a-blockchain/simulate-network/). - -## Template Structure - -A Substrate project such as this consists of a number of components that are -spread across a few directories. - -### Node - -A blockchain node is an application that allows users to participate in a -blockchain network. Substrate-based blockchain nodes expose a number of -capabilities: - -- Networking: Substrate nodes use the [`libp2p`](https://libp2p.io/) networking - stack to allow the nodes in the network to communicate with one another. -- Consensus: Blockchains must have a way to come to - [consensus](https://docs.substrate.io/fundamentals/consensus/) on the state of - the network. Substrate makes it possible to supply custom consensus engines - and also ships with several consensus mechanisms that have been built on top - of [Web3 Foundation - research](https://research.web3.foundation/Polkadot/protocols/NPoS). -- RPC Server: A remote procedure call (RPC) server is used to interact with - Substrate nodes. - -There are several files in the `node` directory. Take special note of the -following: - -- [`chain_spec.rs`](./node/src/chain_spec.rs): A [chain - specification](https://docs.substrate.io/build/chain-spec/) is a source code - file that defines a Substrate chain's initial (genesis) state. Chain - specifications are useful for development and testing, and critical when - architecting the launch of a production chain. Take note of the - `development_config` and `testnet_genesis` functions. These functions are - used to define the genesis state for the local development chain - configuration. These functions identify some [well-known - accounts](https://docs.substrate.io/reference/command-line-tools/subkey/) and - use them to configure the blockchain's initial state. -- [`service.rs`](./node/src/service.rs): This file defines the node - implementation. Take note of the libraries that this file imports and the - names of the functions it invokes. In particular, there are references to - consensus-related topics, such as the [block finalization and - forks](https://docs.substrate.io/fundamentals/consensus/#finalization-and-forks) - and other [consensus - mechanisms](https://docs.substrate.io/fundamentals/consensus/#default-consensus-models) - such as Aura for block authoring and GRANDPA for finality. - - -### Runtime - -In Substrate, the terms "runtime" and "state transition function" are analogous. -Both terms refer to the core logic of the blockchain that is responsible for -validating blocks and executing the state changes they define. The Substrate -project in this repository uses -[FRAME](https://docs.substrate.io/learn/runtime-development/#frame) to construct -a blockchain runtime. FRAME allows runtime developers to declare domain-specific -logic in modules called "pallets". At the heart of FRAME is a helpful [macro -language](https://docs.substrate.io/reference/frame-macros/) that makes it easy -to create pallets and flexibly compose them to create blockchains that can -address [a variety of needs](https://substrate.io/ecosystem/projects/). - -Review the [FRAME runtime implementation](./runtime/src/lib.rs) included in this -template and note the following: - -- This file configures several pallets to include in the runtime. Each pallet - configuration is defined by a code block that begins with `impl - $PALLET_NAME::Config for Runtime`. -- The pallets are composed into a single runtime by way of the - [#[runtime]](https://paritytech.github.io/polkadot-sdk/master/frame_support/attr.runtime.html) - macro, which is part of the [core FRAME pallet - library](https://docs.substrate.io/reference/frame-pallets/#system-pallets). - -### Pallets - -The runtime in this project is constructed using many FRAME pallets that ship -with [the Substrate -repository](https://github.com/paritytech/polkadot-sdk/tree/master/substrate/frame) and a -template pallet that is [defined in the -`pallets`](./pallets/template/src/lib.rs) directory. - -A FRAME pallet is comprised of a number of blockchain primitives, including: - -- Storage: FRAME defines a rich set of powerful [storage - abstractions](https://docs.substrate.io/build/runtime-storage/) that makes it - easy to use Substrate's efficient key-value database to manage the evolving - state of a blockchain. -- Dispatchables: FRAME pallets define special types of functions that can be - invoked (dispatched) from outside of the runtime in order to update its state. -- Events: Substrate uses - [events](https://docs.substrate.io/build/events-and-errors/) to notify users - of significant state changes. -- Errors: When a dispatchable fails, it returns an error. - -Each pallet has its own `Config` trait which serves as a configuration interface -to generically define the types and parameters it depends on. - -## Alternatives Installations - -Instead of installing dependencies and building this source directly, consider -the following alternatives. - -### Nix - -Install [nix](https://nixos.org/) and -[nix-direnv](https://github.com/nix-community/nix-direnv) for a fully -plug-and-play experience for setting up the development environment. To get all -the correct dependencies, activate direnv `direnv allow`. - -### Docker - -Please follow the [Substrate Docker instructions -here](https://github.com/paritytech/polkadot-sdk/blob/master/substrate/docker/README.md) to -build the Docker container with the Substrate Node Template binary. +## Events + +- `OrderPlaced`: New order submitted +- `TradeExecuted`: Trade matched and executed +- `OrderFilled`: Order completely filled +- `OrderPartiallyFilled`: Order partially filled +- `OrderCancelled`: Order cancelled by user +- `CancellationRequested`: Cancellation queued for processing +- `MatchingCompleted`: Block finalization complete (summary stats) + +## Configuration + +Configure constants in your runtime: + +```rust +impl pallet_orderbook::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type MaxPendingOrders = ConstU32<1000>; // Max orders per block + type MaxCancellationOrders = ConstU32<100>; // Max cancellations per block + type MaxOrders = ConstU32<10000>; // Max orders per price level + type MaxUserOrders = ConstU32<1000>; // Max orders per user +} +``` + +## TODO + +- [ ] Order pruning/expiry logic (TTL-based) +- [ ] Mock runtime for testing +- [ ] Comprehensive unit tests +- [ ] Benchmark weights +- [ ] Stop-loss orders +- [ ] Good-til-cancelled (GTC) orders +- [ ] Fill-or-kill (FOK) orders +- [ ] Immediate-or-cancel (IOC) orders + +## Key Design Decisions + +- **Batch matching**: All orders matched once per block (not per-order) +- **Two-phase matching**: Pending orders match internally before checking persistent orderbook +- **OrderId-based storage**: Orderbook stores IDs, not full Order structs (saves space) +- **BoundedVec for cache**: DoS prevention on temporary storage +- **Vec for persistent**: Orders naturally accumulate, no hard limit +- **Price-time priority**: Standard exchange rules (best price first, FIFO within price level) + +## License + +Unlicense + +## Resources + +- [Substrate Documentation](https://docs.substrate.io/) +- [Polkadot-JS Apps](https://polkadot.js.org/apps/) +- [FRAME Development](https://docs.substrate.io/learn/runtime-development/) \ No newline at end of file From eb4defa74731b82a3366c92677f4d61f2937013f Mon Sep 17 00:00:00 2001 From: randalllionelkharkrang Date: Tue, 14 Oct 2025 14:28:47 +0530 Subject: [PATCH 05/15] Added mock runtime with tests for orderbook --- Cargo.lock | 1 + Cargo.toml | 2 +- pallets/orderbook/src/engine.rs | 13 +- pallets/orderbook/src/lib.rs | 67 ++- pallets/orderbook/src/mock.rs | 74 ++- pallets/orderbook/src/tests.rs | 935 +++++++++++++++++++++++++++++++- runtime/Cargo.toml | 4 + runtime/src/configs/mod.rs | 15 + runtime/src/lib.rs | 3 + 9 files changed, 1098 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5ee73a8..fa0ead9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9502,6 +9502,7 @@ dependencies = [ "pallet-aura", "pallet-balances", "pallet-grandpa", + "pallet-orderbook", "pallet-sudo", "pallet-template", "pallet-timestamp", diff --git a/Cargo.toml b/Cargo.toml index 8e6efc4..6f6586c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,7 +63,7 @@ pallet-aura = { version = "39.0.0", default-features = false } pallet-assets = { version = "0.1.0", path = "./pallets/assets", default-features = false } pallet-balances = { version = "41.1.0", default-features = false } pallet-grandpa = { version = "40.0.0", default-features = false } -pallet-orderbook = {version = "0.1.0", default-features = false} +pallet-orderbook = {version = "0.1.0", path="./pallets/orderbook" , default-features = false} pallet-sudo = { version = "40.0.0", default-features = false } pallet-timestamp = { version = "39.0.0", default-features = false } pallet-transaction-payment-rpc-runtime-api = { version = "40.0.0", default-features = false } diff --git a/pallets/orderbook/src/engine.rs b/pallets/orderbook/src/engine.rs index 05f3937..dbe9c77 100644 --- a/pallets/orderbook/src/engine.rs +++ b/pallets/orderbook/src/engine.rs @@ -52,11 +52,10 @@ pub fn match_pending_internal( + orders_map.insert(order_id, order.clone()); + if order.status != OrderStatus::Filled { add_order_to_book(&order, &mut bid_book, &mut ask_book); - orders_map.insert(order_id, order.clone()); - } else { - orders_map.remove(&order.order_id); } } @@ -93,11 +92,9 @@ pub fn match_persistent_storage( trades.extend(order_trades.unwrap()); - if order.status == OrderStatus::Filled { - orders_map.remove(&order_id); // Remove filled orders - } else { - orders_map.insert(*order_id, order.clone()); // Keep active orders - // also add to persistent storage + orders_map.insert(*order_id, order.clone()); + + if order.status != OrderStatus::Filled { add_order_to_book(&order, persistent_bids, persistent_asks); } } diff --git a/pallets/orderbook/src/lib.rs b/pallets/orderbook/src/lib.rs index 2d809cc..db87afb 100644 --- a/pallets/orderbook/src/lib.rs +++ b/pallets/orderbook/src/lib.rs @@ -215,6 +215,7 @@ pub mod pallet { NoMatchingOrders, } + // ======================================== // HOOKS FOR MATCHING @@ -222,7 +223,7 @@ pub mod pallet { #[pallet::hooks] impl Hooks> for Pallet { - fn on_finalize(_n: BlockNumberFor) { + fn on_finalize(_n: BlockNumberFor) { let mut orders_map = BTreeMap::new(); //load all orders, will need to modify for sure @@ -294,7 +295,7 @@ pub mod pallet { all_trades.extend(persistent_trades); } - /// At this point, we have in memory done all necessary transactions + // At this point, we have in memory done all necessary transactions // Now we need to adjust order/money management let mut total_volume = 0u128; @@ -320,9 +321,10 @@ pub mod pallet { trade.quantity, ); - // Unlock funds for both parties - let _ = assets::Pallet::::unlock_funds(&trade.seller, USDT, usdt_amount); - let _ = assets::Pallet::::unlock_funds(&trade.buyer, ETH, trade.quantity); + // Unlock funds for both parties(NOt required i realized that transfer_locked alredy transfer + //to free balance, unlocking might unlock some other things not in the trade) + //let _ = assets::Pallet::::unlock_funds(&trade.seller, USDT, usdt_amount); + //let _ = assets::Pallet::::unlock_funds(&trade.buyer, ETH, trade.quantity); // Store trade Trades::::insert(trade_id, trade.clone()); @@ -449,6 +451,7 @@ pub mod pallet { #[pallet::call] impl Pallet { + /// Place a limit order #[pallet::call_index(0)] #[pallet::weight(10000)] @@ -534,4 +537,58 @@ pub mod pallet { Ok(()) } } + + // ====================================== + // Getter functions for storage/for some reason, directly acccessing them doesnt work + // ======================================= + impl Pallet { + pub fn next_order_id() -> OrderId { + NextOrderId::::get() + } + + /// Get the next trade ID + pub fn next_trade_id() -> TradeId { + NextTradeId::::get() + } + + /// Get an order by ID + pub fn get_order(order_id: OrderId) -> Option> { + Orders::::get(order_id) + } + + /// Get a trade by ID + pub fn get_trade(trade_id: TradeId) -> Option> { + Trades::::get(trade_id) + } + + /// Get bids at a specific price level + pub fn get_bids_at_price(price: Amount) -> Vec { + Bids::::get(price).into_inner() + } + + /// Get asks at a specific price level + pub fn get_asks_at_price(price: Amount) -> Vec { + Asks::::get(price).into_inner() + } + + /// Get pending bids at a specific price level + pub fn get_pending_bids_at_price(price: Amount) -> Vec { + PendingBids::::get(price).into_inner() + } + + /// Get pending asks at a specific price level + pub fn get_pending_asks_at_price(price: Amount) -> Vec { + PendingAsks::::get(price).into_inner() + } + + /// Get pending cancellations + pub fn get_pending_cancellations() -> Vec { + PendingCancellations::::get().into_inner() + } + + /// Get user's orders + pub fn get_user_orders(user: &T::AccountId) -> Vec { + UserOrders::::get(user).into_inner() + } + } } \ No newline at end of file diff --git a/pallets/orderbook/src/mock.rs b/pallets/orderbook/src/mock.rs index a4fba45..71273f6 100644 --- a/pallets/orderbook/src/mock.rs +++ b/pallets/orderbook/src/mock.rs @@ -1 +1,73 @@ -//to be impl \ No newline at end of file +use frame_support::derive_impl; +use frame_system::pallet; +use sp_runtime::BuildStorage; +use crate as pallet_orderbook; +use sp_runtime::traits::parameter_types; + + +type Block = frame_system::mocking::MockBlock; + +#[frame_support::runtime] +mod runtime { + use frame_support::runtime; + + #[runtime::runtime] + #[runtime::derive( + RuntimeCall, + RuntimeError, + RuntimeEvent, + RuntimeOrigin, + RuntimeFreezeReason, + RuntimeHoldReason, + RuntimeSlashReason, + RuntimeLockId, + RuntimeTask, + )] + + pub struct Test; + + #[runtime::pallet_index(0)] + pub type System = frame_system::Pallet; + + #[runtime::pallet_index(1)] + pub type Assets = pallet_assets::Pallet; + + #[runtime::pallet_index(2)] + pub type Orderbook = pallet_orderbook::Pallet; + +} + +#[derive_impl(frame_system::config_preludes::TestDefaultConfig)] +impl frame_system::Config for Test { + type Block = Block; +} + +impl pallet_assets::Config for Test{ + type RuntimeEvent = RuntimeEvent; + type WeightInfo = (); +} + +parameter_types! { + pub const MaxPendingOrders: u32 = 100; // Max 100 pending orders per block in tests + pub const MaxCancellationOrders: u32 = 50; // Max 50 cancellations per block in tests + pub const MaxOrders: u32 = 1000; // Max 1000 orders per price level in tests + pub const MaxUserOrders: u32 = 100; // Max 100 orders per user in tests +} + +impl pallet_orderbook::Config for Test { + type RuntimeEvent = RuntimeEvent; + type MaxPendingOrders = MaxPendingOrders; + type MaxCancellationOrders = MaxCancellationOrders; + type MaxOrders = MaxOrders; + type MaxUserOrders = MaxUserOrders; +} + + +pub fn new_test_ext() -> sp_io::TestExternalities { + frame_system::GenesisConfig::::default() + .build_storage() + .unwrap() + .into() +} + + diff --git a/pallets/orderbook/src/tests.rs b/pallets/orderbook/src/tests.rs index a4fba45..a0ec505 100644 --- a/pallets/orderbook/src/tests.rs +++ b/pallets/orderbook/src/tests.rs @@ -1 +1,934 @@ -//to be impl \ No newline at end of file +use crate::mock::*; +use crate::types::*; +use frame_support::{assert_ok, assert_noop, traits::Hooks}; +use pallet_assets::{USDT, ETH}; + +// Simple u64 accounts for testing +fn alice() -> u64 { + 1 +} + +fn bob() -> u64 { + 2 +} + +fn charlie() -> u64 { + 3 +} + +// Helper to fund accounts +fn fund_account(account: u64, usdt: u128, eth: u128) { + if usdt > 0 { + assert_ok!(Assets::deposit( + RuntimeOrigin::signed(account), + USDT, + usdt + )); + } + if eth > 0 { + assert_ok!(Assets::deposit( + RuntimeOrigin::signed(account), + ETH, + eth + )); + } +} + +// ============================================ +// BASIC ORDER PLACEMENT TESTS +// ============================================ + +#[test] +fn test_place_buy_order_works() { + new_test_ext().execute_with(|| { + let alice = alice(); + fund_account(alice, 10_000, 0); + + // Place buy order: 10 ETH @ $100 + assert_ok!(Orderbook::place_order( + RuntimeOrigin::signed(alice), + OrderSide::Buy, + 100, + 10, + OrderType::Limit, + )); + + // Check order was created + let order = Orderbook::get_order(0).expect("Order should exist"); + assert_eq!(order.trader, alice); + assert_eq!(order.side, OrderSide::Buy); + assert_eq!(order.price, 100); + assert_eq!(order.quantity, 10); + assert_eq!(order.status, OrderStatus::Open); + + // Check order ID incremented + assert_eq!(Orderbook::next_order_id(), 1); + + // Check funds were locked (10 ETH * $100 = 1000 USDT) + assert_eq!(Assets::get_free_balance(&alice, USDT), 9_000); + assert_eq!(Assets::get_locked_balance(&alice, USDT), 1_000); + + // Check order was added to pending bids + let pending = Orderbook::get_pending_bids_at_price(100); + assert_eq!(pending.len(), 1); + assert_eq!(pending[0], 0); + }); +} + +#[test] +fn test_place_sell_order_works() { + new_test_ext().execute_with(|| { + let alice = alice(); + fund_account(alice, 0, 100); + + // Place sell order: 10 ETH @ $100 + assert_ok!(Orderbook::place_order( + RuntimeOrigin::signed(alice), + OrderSide::Sell, + 100, + 10, + OrderType::Limit, + )); + + // Check order was created + let order = Orderbook::get_order(0).expect("Order should exist"); + assert_eq!(order.side, OrderSide::Sell); + assert_eq!(order.price, 100); + + // Check funds were locked + assert_eq!(Assets::get_free_balance(&alice, ETH), 90); + assert_eq!(Assets::get_locked_balance(&alice, ETH), 10); + + // Check order was added to pending asks + let pending = Orderbook::get_pending_asks_at_price(100); + assert_eq!(pending.len(), 1); + assert_eq!(pending[0], 0); + }); +} + +#[test] +fn test_place_multiple_orders_same_price() { + new_test_ext().execute_with(|| { + let alice = alice(); + let bob = bob(); + fund_account(alice, 10_000, 0); + fund_account(bob, 10_000, 0); + + // Alice places buy order + assert_ok!(Orderbook::place_order( + RuntimeOrigin::signed(alice), + OrderSide::Buy, + 100, + 5, + OrderType::Limit, + )); + + // Bob places buy order at same price + assert_ok!(Orderbook::place_order( + RuntimeOrigin::signed(bob), + OrderSide::Buy, + 100, + 10, + OrderType::Limit, + )); + + // Check both orders in pending bids + let pending = Orderbook::get_pending_bids_at_price(100); + assert_eq!(pending.len(), 2); + assert_eq!(pending[0], 0); // Alice first (FIFO) + assert_eq!(pending[1], 1); // Bob second + + // Check next order ID + assert_eq!(Orderbook::next_order_id(), 2); + }); +} + +#[test] +fn test_place_market_order() { + new_test_ext().execute_with(|| { + let alice = alice(); + fund_account(alice, 10_000, 0); + + // Place market buy order (price is ignored) + assert_ok!(Orderbook::place_order( + RuntimeOrigin::signed(alice), + OrderSide::Buy, + 200, + 10, + OrderType::Market, + )); + + let order = Orderbook::get_order(0).expect("Order should exist"); + assert_eq!(order.order_type, OrderType::Market); + }); +} + +// ============================================ +// VALIDATION TESTS +// ============================================ + +#[test] +fn test_place_order_invalid_price_fails() { + new_test_ext().execute_with(|| { + let alice = alice(); + fund_account(alice, 10_000, 0); + + assert_noop!( + Orderbook::place_order( + RuntimeOrigin::signed(alice), + OrderSide::Buy, + 0, // Invalid price for limit order + 10, + OrderType::Limit, + ), + crate::Error::::InvalidPrice + ); + }); +} + +#[test] +fn test_place_order_invalid_quantity_fails() { + new_test_ext().execute_with(|| { + let alice = alice(); + fund_account(alice, 10_000, 0); + + assert_noop!( + Orderbook::place_order( + RuntimeOrigin::signed(alice), + OrderSide::Buy, + 100, + 0, // Invalid quantity + OrderType::Limit, + ), + crate::Error::::InvalidQuantity + ); + }); +} + +#[test] +fn test_place_order_insufficient_balance_fails() { + new_test_ext().execute_with(|| { + let alice = alice(); + fund_account(alice, 500, 0); // Only 500 USDT + + // Try to buy 10 ETH @ $100 (needs 1000 USDT) + assert_noop!( + Orderbook::place_order( + RuntimeOrigin::signed(alice), + OrderSide::Buy, + 100, + 10, + OrderType::Limit, + ), + pallet_assets::Error::::InsufficientFreeBalance + ); + }); +} + +#[test] +fn test_place_order_arithmetic_overflow() { + new_test_ext().execute_with(|| { + let alice = alice(); + fund_account(alice, u128::MAX, 0); + + // Try to create order that would overflow + assert_noop!( + Orderbook::place_order( + RuntimeOrigin::signed(alice), + OrderSide::Buy, + u128::MAX, + u128::MAX, // This would overflow when multiplied + OrderType::Limit, + ), + crate::Error::::ArithmeticOverflow + ); + }); +} + +// ============================================ +// CANCELLATION TESTS +// ============================================ + +#[test] +fn test_cancel_order_works() { + new_test_ext().execute_with(|| { + let alice = alice(); + fund_account(alice, 10_000, 0); + + // Place order + assert_ok!(Orderbook::place_order( + RuntimeOrigin::signed(alice), + OrderSide::Buy, + 100, + 10, + OrderType::Limit, + )); + + // Cancel order + assert_ok!(Orderbook::cancel_order( + RuntimeOrigin::signed(alice), + 0, // order_id + )); + + // Check cancellation was queued + let cancellations = Orderbook::get_pending_cancellations(); + assert_eq!(cancellations.len(), 1); + assert_eq!(cancellations[0], 0); + }); +} + +#[test] +fn test_cancel_order_not_owner_fails() { + new_test_ext().execute_with(|| { + let alice = alice(); + let bob = bob(); + fund_account(alice, 10_000, 0); + + // Alice places order + assert_ok!(Orderbook::place_order( + RuntimeOrigin::signed(alice), + OrderSide::Buy, + 100, + 10, + OrderType::Limit, + )); + + // Bob tries to cancel Alice's order - should fail + assert_noop!( + Orderbook::cancel_order( + RuntimeOrigin::signed(bob), + 0, + ), + crate::Error::::NotOrderOwner + ); + }); +} + +#[test] +fn test_cancel_nonexistent_order_fails() { + new_test_ext().execute_with(|| { + let alice = alice(); + + assert_noop!( + Orderbook::cancel_order( + RuntimeOrigin::signed(alice), + 999, // Doesn't exist + ), + crate::Error::::OrderNotFound + ); + }); +} + +#[test] +fn test_cancel_multiple_orders() { + new_test_ext().execute_with(|| { + let alice = alice(); + fund_account(alice, 10_000, 0); + + // Place 3 orders + for i in 0..3 { + assert_ok!(Orderbook::place_order( + RuntimeOrigin::signed(alice), + OrderSide::Buy, + 100 + i as u128, + 10, + OrderType::Limit, + )); + } + + // Cancel first and third order + assert_ok!(Orderbook::cancel_order(RuntimeOrigin::signed(alice), 0)); + assert_ok!(Orderbook::cancel_order(RuntimeOrigin::signed(alice), 2)); + + // Check both cancellations queued + let cancellations = Orderbook::get_pending_cancellations(); + assert_eq!(cancellations.len(), 2); + assert_eq!(cancellations[0], 0); + assert_eq!(cancellations[1], 2); + }); +} + +// ============================================ +// EDGE CASE TESTS +// ============================================ + +#[test] +fn test_place_order_with_exact_balance() { + new_test_ext().execute_with(|| { + let alice = alice(); + fund_account(alice, 1_000, 0); // Exactly 1000 USDT + + // Buy exactly what we can afford + assert_ok!(Orderbook::place_order( + RuntimeOrigin::signed(alice), + OrderSide::Buy, + 100, + 10, // Exactly 1000 USDT needed + OrderType::Limit, + )); + + // Should have locked all funds + assert_eq!(Assets::get_free_balance(&alice, USDT), 0); + assert_eq!(Assets::get_locked_balance(&alice, USDT), 1_000); + }); +} + +#[test] +fn test_place_order_one_wei_short_fails() { + new_test_ext().execute_with(|| { + let alice = alice(); + fund_account(alice, 999, 0); // One less than needed + + assert_noop!( + Orderbook::place_order( + RuntimeOrigin::signed(alice), + OrderSide::Buy, + 100, + 10, // Needs 1000 USDT + OrderType::Limit, + ), + pallet_assets::Error::::InsufficientFreeBalance + ); + }); +} + +#[test] +fn test_sequential_orders_increment_ids() { + new_test_ext().execute_with(|| { + let alice = alice(); + fund_account(alice, 100_000, 1000); + + // Place 5 orders + for i in 0..5 { + assert_ok!(Orderbook::place_order( + RuntimeOrigin::signed(alice), + if i % 2 == 0 { OrderSide::Buy } else { OrderSide::Sell }, + 100, + 1, + OrderType::Limit, + )); + + // Check ID incremented correctly + assert_eq!(Orderbook::next_order_id(), i + 1); + + // Check order exists with correct ID + let order = Orderbook::get_order(i).expect("Order should exist"); + assert_eq!(order.order_id, i); + } + }); +} + +#[test] +fn test_large_order_values() { + new_test_ext().execute_with(|| { + let alice = alice(); + fund_account(alice, 1_000_000_000, 0); // 1 billion USDT + + // Place large order + assert_ok!(Orderbook::place_order( + RuntimeOrigin::signed(alice), + OrderSide::Buy, + 10_000, // $10,000 per ETH + 100_000, // 100k ETH + OrderType::Limit, + )); + + // Check huge amount locked (10k * 100k = 1 billion) + assert_eq!(Assets::get_locked_balance(&alice, USDT), 1_000_000_000); + }); +} + +#[test] +fn test_different_users_different_orders() { + new_test_ext().execute_with(|| { + let alice = alice(); + let bob = bob(); + let charlie = charlie(); + + fund_account(alice, 10_000, 0); + fund_account(bob, 0, 100); + fund_account(charlie, 5_000, 50); + + // Each user places different order + assert_ok!(Orderbook::place_order( + RuntimeOrigin::signed(alice), + OrderSide::Buy, + 100, + 10, + OrderType::Limit, + )); + + assert_ok!(Orderbook::place_order( + RuntimeOrigin::signed(bob), + OrderSide::Sell, + 105, + 20, + OrderType::Limit, + )); + + assert_ok!(Orderbook::place_order( + RuntimeOrigin::signed(charlie), + OrderSide::Buy, + 98, + 5, + OrderType::Limit, + )); + + // Verify each order has correct owner + assert_eq!(Orderbook::get_order(0).unwrap().trader, alice); + assert_eq!(Orderbook::get_order(1).unwrap().trader, bob); + assert_eq!(Orderbook::get_order(2).unwrap().trader, charlie); + + // Verify 3 orders created + assert_eq!(Orderbook::next_order_id(), 3); + }); +} + + +#[test] +fn test_simple_buy_sell_match() { + new_test_ext().execute_with(|| { + let alice = alice(); + let bob = bob(); + + // Setup: Give Alice USDT, Bob ETH + fund_account(alice, 10_000, 0); + fund_account(bob, 0, 100); + + // Alice: Buy 10 ETH @ $100 + assert_ok!(Orderbook::place_order( + RuntimeOrigin::signed(alice), + OrderSide::Buy, + 100, + 10, + OrderType::Limit, + )); + + // Bob: Sell 10 ETH @ $100 + assert_ok!(Orderbook::place_order( + RuntimeOrigin::signed(bob), + OrderSide::Sell, + 100, + 10, + OrderType::Limit, + )); + + // Both orders pending + assert_eq!(Orderbook::get_pending_bids_at_price(100).len(), 1); + assert_eq!(Orderbook::get_pending_asks_at_price(100).len(), 1); + + // Trigger matching by advancing to next block + System::set_block_number(1); + Orderbook::on_finalize(1); + + // Verify trade executed + let trade = Orderbook::get_trade(0).expect("Trade should exist"); + assert_eq!(trade.buyer, alice); + assert_eq!(trade.seller, bob); + assert_eq!(trade.price, 100); + assert_eq!(trade.quantity, 10); + + // Verify balances after settlement + // Alice: spent 1000 USDT, got 10 ETH + assert_eq!(Assets::get_free_balance(&alice, USDT), 9_000); + assert_eq!(Assets::get_free_balance(&alice, ETH), 10); + assert_eq!(Assets::get_locked_balance(&alice, USDT), 0); + assert_eq!(Assets::get_locked_balance(&alice, ETH), 0); + + // Bob: got 1000 USDT, spent 10 ETH + assert_eq!(Assets::get_free_balance(&bob, USDT), 1_000); + assert_eq!(Assets::get_free_balance(&bob, ETH), 90); + assert_eq!(Assets::get_locked_balance(&bob, USDT), 0); + assert_eq!(Assets::get_locked_balance(&bob, ETH), 0); + + // Verify orders are filled + let alice_order = Orderbook::get_order(0).unwrap(); + assert_eq!(alice_order.status, OrderStatus::Filled); + assert_eq!(alice_order.filled_quantity, 10); + + let bob_order = Orderbook::get_order(1).unwrap(); + assert_eq!(bob_order.status, OrderStatus::Filled); + assert_eq!(bob_order.filled_quantity, 10); + + // Verify trade ID incremented + assert_eq!(Orderbook::next_trade_id(), 1); + }); +} + +#[test] +#[test] +fn test_partial_fill_matching_debug() { + new_test_ext().execute_with(|| { + let alice = alice(); + let bob = bob(); + + fund_account(alice, 10_000, 0); + fund_account(bob, 0, 100); + + println!("=== Initial state ==="); + println!("Alice USDT: {}", Assets::get_free_balance(&alice, USDT)); + println!("Bob ETH: {}", Assets::get_free_balance(&bob, ETH)); + + // Alice: Buy 10 ETH @ $100 + assert_ok!(Orderbook::place_order( + RuntimeOrigin::signed(alice), + OrderSide::Buy, + 100, + 10, + OrderType::Limit, + )); + + println!("\n=== After Alice order ==="); + println!("Alice free USDT: {}", Assets::get_free_balance(&alice, USDT)); + println!("Alice locked USDT: {}", Assets::get_locked_balance(&alice, USDT)); + + // Bob: Sell only 5 ETH @ $100 + assert_ok!(Orderbook::place_order( + RuntimeOrigin::signed(bob), + OrderSide::Sell, + 100, + 5, + OrderType::Limit, + )); + + println!("\n=== After Bob order ==="); + println!("Bob free ETH: {}", Assets::get_free_balance(&bob, ETH)); + println!("Bob locked ETH: {}", Assets::get_locked_balance(&bob, ETH)); + + >::on_finalize(1); + + println!("\n=== After matching ==="); + let alice_order = Orderbook::get_order(0).unwrap(); + println!("Alice order status: {:?}", alice_order.status); + println!("Alice filled: {}/{}", alice_order.filled_quantity, alice_order.quantity); + + let bob_order = Orderbook::get_order(1).unwrap(); + println!("Bob order status: {:?}", bob_order.status); + println!("Bob filled: {}/{}", bob_order.filled_quantity, bob_order.quantity); + + println!("\n=== Final balances ==="); + println!("Alice free USDT: {}", Assets::get_free_balance(&alice, USDT)); + println!("Alice locked USDT: {}", Assets::get_locked_balance(&alice, USDT)); + println!("Alice free ETH: {}", Assets::get_free_balance(&alice, ETH)); + println!("Alice locked ETH: {}", Assets::get_locked_balance(&alice, ETH)); + + println!("Bob free USDT: {}", Assets::get_free_balance(&bob, USDT)); + println!("Bob locked USDT: {}", Assets::get_locked_balance(&bob, USDT)); + println!("Bob free ETH: {}", Assets::get_free_balance(&bob, ETH)); + println!("Bob locked ETH: {}", Assets::get_locked_balance(&bob, ETH)); + + let trade = Orderbook::get_trade(0).unwrap(); + println!("\nTrade: {} ETH @ ${}", trade.quantity, trade.price); + }); +} +#[test] +fn test_price_time_priority_fifo() { + new_test_ext().execute_with(|| { + let alice = alice(); + let bob = bob(); + let charlie = charlie(); + + fund_account(alice, 0, 100); + fund_account(bob, 0, 100); + fund_account(charlie, 10_000, 0); + + // Alice: Sell 5 ETH @ $100 (FIRST) + assert_ok!(Orderbook::place_order( + RuntimeOrigin::signed(alice), + OrderSide::Sell, + 100, + 5, + OrderType::Limit, + )); + + // Bob: Sell 5 ETH @ $100 (SECOND - same price, later time) + assert_ok!(Orderbook::place_order( + RuntimeOrigin::signed(bob), + OrderSide::Sell, + 100, + 5, + OrderType::Limit, + )); + + // Charlie: Buy 5 ETH @ $100 + assert_ok!(Orderbook::place_order( + RuntimeOrigin::signed(charlie), + OrderSide::Buy, + 100, + 5, + OrderType::Limit, + )); + + // Trigger matching + System::set_block_number(1); + Orderbook::on_finalize(1); + + // Should match with Alice (FIFO - first in, first out) + let trade = Orderbook::get_trade(0).unwrap(); + assert_eq!(trade.seller, alice); // Alice matched, not Bob + assert_eq!(trade.buyer, charlie); + + // Alice's order filled, Bob's still open + let alice_order = Orderbook::get_order(0).unwrap(); + assert_eq!(alice_order.status, OrderStatus::Filled); + + let bob_order = Orderbook::get_order(1).unwrap(); + assert_eq!(bob_order.status, OrderStatus::Open); // Still waiting! + + // Verify Alice got paid + assert_eq!(Assets::get_free_balance(&alice, USDT), 500); + assert_eq!(Assets::get_free_balance(&alice, ETH), 95); + + // Bob didn't trade yet + assert_eq!(Assets::get_free_balance(&bob, USDT), 0); + assert_eq!(Assets::get_locked_balance(&bob, ETH), 5); // Still locked + }); +} + +#[test] +fn test_no_match_price_spread() { + new_test_ext().execute_with(|| { + let alice = alice(); + let bob = bob(); + + fund_account(alice, 10_000, 0); + fund_account(bob, 0, 100); + + // Alice: Buy @ $95 + assert_ok!(Orderbook::place_order( + RuntimeOrigin::signed(alice), + OrderSide::Buy, + 95, + 10, + OrderType::Limit, + )); + + // Bob: Sell @ $105 (no match - spread too wide) + assert_ok!(Orderbook::place_order( + RuntimeOrigin::signed(bob), + OrderSide::Sell, + 105, + 10, + OrderType::Limit, + )); + + // Trigger matching + System::set_block_number(1); + Orderbook::on_finalize(1); + + // No trades should execute + assert!(Orderbook::get_trade(0).is_none()); + + // Both orders should remain open + let alice_order = Orderbook::get_order(0).unwrap(); + assert_eq!(alice_order.status, OrderStatus::Open); + assert_eq!(alice_order.filled_quantity, 0); + + let bob_order = Orderbook::get_order(1).unwrap(); + assert_eq!(bob_order.status, OrderStatus::Open); + assert_eq!(bob_order.filled_quantity, 0); + + // Funds still locked + assert_eq!(Assets::get_locked_balance(&alice, USDT), 950); + assert_eq!(Assets::get_locked_balance(&bob, ETH), 10); + }); +} + +#[test] +fn test_multiple_trades_same_block() { + new_test_ext().execute_with(|| { + let alice = alice(); + let bob = bob(); + let charlie = charlie(); + + fund_account(alice, 10_000, 0); + fund_account(bob, 0, 50); + fund_account(charlie, 0, 50); + + // Alice: Buy 20 ETH @ $100 + assert_ok!(Orderbook::place_order( + RuntimeOrigin::signed(alice), + OrderSide::Buy, + 100, + 20, + OrderType::Limit, + )); + + // Bob: Sell 10 ETH @ $100 + assert_ok!(Orderbook::place_order( + RuntimeOrigin::signed(bob), + OrderSide::Sell, + 100, + 10, + OrderType::Limit, + )); + + // Charlie: Sell 10 ETH @ $100 + assert_ok!(Orderbook::place_order( + RuntimeOrigin::signed(charlie), + OrderSide::Sell, + 100, + 10, + OrderType::Limit, + )); + + // Trigger matching + System::set_block_number(1); + Orderbook::on_finalize(1); + + // Should create 2 trades (Alice with Bob, Alice with Charlie) + assert!(Orderbook::get_trade(0).is_some()); + assert!(Orderbook::get_trade(1).is_some()); + + let trade1 = Orderbook::get_trade(0).unwrap(); + let trade2 = Orderbook::get_trade(1).unwrap(); + + // Both trades with Alice as buyer + assert_eq!(trade1.buyer, alice); + assert_eq!(trade2.buyer, alice); + + // Bob and Charlie as sellers + assert!(trade1.seller == bob || trade2.seller == bob); + assert!(trade1.seller == charlie || trade2.seller == charlie); + + // Alice's order should be fully filled (20 ETH total) + let alice_order = Orderbook::get_order(0).unwrap(); + assert_eq!(alice_order.status, OrderStatus::Filled); + assert_eq!(alice_order.filled_quantity, 20); + + // Alice should have 20 ETH, spent 2000 USDT + assert_eq!(Assets::get_free_balance(&alice, ETH), 20); + assert_eq!(Assets::get_free_balance(&alice, USDT), 8_000); + assert_eq!(Assets::get_locked_balance(&alice, USDT), 0); + + // Bob got 1000 USDT + assert_eq!(Assets::get_free_balance(&bob, USDT), 1_000); + + // Charlie got 1000 USDT + assert_eq!(Assets::get_free_balance(&charlie, USDT), 1_000); + }); +} + +#[test] +fn test_cancellation_unlocks_funds() { + new_test_ext().execute_with(|| { + let alice = alice(); + fund_account(alice, 10_000, 0); + + // Place order + assert_ok!(Orderbook::place_order( + RuntimeOrigin::signed(alice), + OrderSide::Buy, + 100, + 10, + OrderType::Limit, + )); + + // Verify funds locked + assert_eq!(Assets::get_locked_balance(&alice, USDT), 1_000); + + // Cancel order + assert_ok!(Orderbook::cancel_order( + RuntimeOrigin::signed(alice), + 0, + )); + + // Trigger finalization to process cancellation + System::set_block_number(1); + Orderbook::on_finalize(1); + + // Verify funds unlocked + assert_eq!(Assets::get_free_balance(&alice, USDT), 10_000); + assert_eq!(Assets::get_locked_balance(&alice, USDT), 0); + + // Verify order cancelled + let order = Orderbook::get_order(0).unwrap(); + assert_eq!(order.status, OrderStatus::Cancelled); + }); +} + +#[test] +#[test] +fn test_market_order_matches_best_price() { + new_test_ext().execute_with(|| { + let alice = alice(); + let bob = bob(); + + fund_account(alice, 0, 100); + fund_account(bob, 10_000, 0); + + assert_ok!(Orderbook::place_order( + RuntimeOrigin::signed(alice), + OrderSide::Sell, + 95, + 10, + OrderType::Limit, + )); + + // For batch matching, market orders still use the price for locking funds + assert_ok!(Orderbook::place_order( + RuntimeOrigin::signed(bob), + OrderSide::Buy, + 95, // Match at same price + 10, + OrderType::Market, + )); + + >::on_finalize(1); + + let trade = Orderbook::get_trade(0).unwrap(); + assert_eq!(trade.price, 95); + + assert_eq!(Assets::get_free_balance(&bob, USDT), 9_050); + assert_eq!(Assets::get_free_balance(&bob, ETH), 10); + }); +} +#[test] +fn test_simple_buy_sell_match_debug() { + new_test_ext().execute_with(|| { + let alice = alice(); + let bob = bob(); + + fund_account(alice, 10_000, 0); + fund_account(bob, 0, 100); + + println!("=== Before orders ==="); + println!("Alice USDT: {}", Assets::get_free_balance(&alice, USDT)); + println!("Bob ETH: {}", Assets::get_free_balance(&bob, ETH)); + + assert_ok!(Orderbook::place_order( + RuntimeOrigin::signed(alice), + OrderSide::Buy, + 100, + 10, + OrderType::Limit, + )); + + assert_ok!(Orderbook::place_order( + RuntimeOrigin::signed(bob), + OrderSide::Sell, + 100, + 10, + OrderType::Limit, + )); + + println!("=== After orders placed ==="); + println!("Pending bids at 100: {:?}", Orderbook::get_pending_bids_at_price(100)); + println!("Pending asks at 100: {:?}", Orderbook::get_pending_asks_at_price(100)); + println!("Order 0: {:?}", Orderbook::get_order(0)); + println!("Order 1: {:?}", Orderbook::get_order(1)); + + // Call on_finalize + println!("=== Calling on_finalize ==="); + >::on_finalize(1); + + println!("=== After on_finalize ==="); + println!("Order 0: {:?}", Orderbook::get_order(0)); + println!("Order 1: {:?}", Orderbook::get_order(1)); + println!("Trade 0: {:?}", Orderbook::get_trade(0)); + println!("Alice USDT: {}", Assets::get_free_balance(&alice, USDT)); + println!("Alice ETH: {}", Assets::get_free_balance(&alice, ETH)); + println!("Bob USDT: {}", Assets::get_free_balance(&bob, USDT)); + println!("Bob ETH: {}", Assets::get_free_balance(&bob, ETH)); + }); +} \ No newline at end of file diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index b863c2d..0beaca0 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -28,6 +28,7 @@ pallet-grandpa.workspace = true pallet-sudo.workspace = true pallet-template.workspace = true pallet-assets.workspace = true +pallet-orderbook.workspace = true pallet-timestamp.workspace = true pallet-transaction-payment-rpc-runtime-api.workspace = true pallet-transaction-payment.workspace = true @@ -69,6 +70,7 @@ std = [ "pallet-sudo/std", "pallet-template/std", "pallet-assets/std", + "pallet-orderbook/std", "pallet-timestamp/std", "pallet-transaction-payment-rpc-runtime-api/std", "pallet-transaction-payment/std", @@ -101,6 +103,7 @@ runtime-benchmarks = [ "pallet-sudo/runtime-benchmarks", "pallet-template/runtime-benchmarks", "pallet-assets/runtime-benchmarks", + "pallet-orderbook/runtime-benchmarks", "pallet-timestamp/runtime-benchmarks", "pallet-transaction-payment/runtime-benchmarks", "sp-runtime/runtime-benchmarks", @@ -117,6 +120,7 @@ try-runtime = [ "pallet-sudo/try-runtime", "pallet-template/try-runtime", "pallet-assets/try-runtime", + "pallet-orderbook/try-runtime", "pallet-timestamp/try-runtime", "pallet-transaction-payment/try-runtime", "sp-runtime/try-runtime", diff --git a/runtime/src/configs/mod.rs b/runtime/src/configs/mod.rs index ae3621b..ab68f6e 100644 --- a/runtime/src/configs/mod.rs +++ b/runtime/src/configs/mod.rs @@ -168,3 +168,18 @@ impl pallet_assets::Config for Runtime { type RuntimeEvent = RuntimeEvent; type WeightInfo = pallet_assets::weights::SubstrateWeight; } + +parameter_types! { + pub const MaxPendingOrders: u32 = 1000; // Max 100 pending orders per block in tests + pub const MaxCancellationOrders: u32 = 50; // Max 50 cancellations per block in tests + pub const MaxOrders: u32 = 10000; // Max 1000 orders per price level in tests + pub const MaxUserOrders: u32 = 1000; // Max 100 orders per user in tests +} + +impl pallet_orderbook::Config for Test { + type RuntimeEvent = RuntimeEvent; + type MaxPendingOrders = MaxPendingOrders; + type MaxCancellationOrders = MaxCancellationOrders; + type MaxOrders = MaxOrders; + type MaxUserOrders = MaxUserOrders; +} diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 18bd604..71f0e9f 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -228,4 +228,7 @@ mod runtime { #[runtime::pallet_index(8)] pub type Assets = pallet_assets; + + #[runtime::pallet_index(9)] + pub type Orderbook = pallet_orderbook; } From 11271588b95cca0673a0d4aa03094f5d82cfa7d6 Mon Sep 17 00:00:00 2001 From: randalllionelkharkrang Date: Wed, 15 Oct 2025 10:59:17 +0530 Subject: [PATCH 06/15] latest commit before benchmark --- Cargo.lock | 1 + Cargo.toml | 1 + pallets/orderbook/Cargo.toml | 3 ++- runtime/Cargo.toml | 2 ++ runtime/src/configs/mod.rs | 10 ++++++++-- runtime/src/lib.rs | 5 ++++- 6 files changed, 18 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fa0ead9..dd16f6e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9522,6 +9522,7 @@ dependencies = [ "sp-offchain", "sp-runtime", "sp-session", + "sp-std", "sp-storage", "sp-transaction-pool", "sp-version", diff --git a/Cargo.toml b/Cargo.toml index 6f6586c..675011e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,7 @@ sp-inherents = { version = "36.0.0", default-features = false } sp-io = { version = "40.0.1", default-features = false } sp-keyring = { version = "41.0.0", default-features = false } sp-runtime = { version = "41.1.0", default-features = false } +sp-std = {version = "14.0.0", default-features = false} sp-timestamp = { version = "36.0.0", default-features = false } substrate-frame-rpc-system = { version = "43.0.0", default-features = false } substrate-build-script-utils = { version = "11.0.0", default-features = false } diff --git a/pallets/orderbook/Cargo.toml b/pallets/orderbook/Cargo.toml index c716fb4..ee54f16 100644 --- a/pallets/orderbook/Cargo.toml +++ b/pallets/orderbook/Cargo.toml @@ -21,7 +21,7 @@ frame-system.workspace = true scale-info = { features = ["derive"], workspace = true } sp-core.workspace = true sp-runtime.workspace = true -sp-std = "14.0.0" +sp-std.workspace = true [dev-dependencies] sp-core = { default-features = true, workspace = true } @@ -36,6 +36,7 @@ std = [ "frame-support/std", "frame-system/std", "scale-info/std", + "sp-std/std", ] runtime-benchmarks = [ "frame-benchmarking/runtime-benchmarks", diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 0beaca0..8f1312e 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -46,6 +46,7 @@ sp-offchain.workspace = true sp-runtime = { features = ["serde"], workspace = true } sp-session.workspace = true sp-storage.workspace = true +sp-std.workspace = true sp-transaction-pool.workspace = true sp-version = { features = ["serde"], workspace = true } @@ -88,6 +89,7 @@ std = [ "sp-runtime/std", "sp-session/std", "sp-storage/std", + "sp-std/std", "sp-transaction-pool/std", "sp-version/std", "substrate-wasm-builder", diff --git a/runtime/src/configs/mod.rs b/runtime/src/configs/mod.rs index ab68f6e..d8c5445 100644 --- a/runtime/src/configs/mod.rs +++ b/runtime/src/configs/mod.rs @@ -38,6 +38,9 @@ use sp_consensus_aura::sr25519::AuthorityId as AuraId; use sp_runtime::{traits::One, Perbill}; use sp_version::RuntimeVersion; +use pallet_assets; +//use pallet_orderbook; + // Local module imports use super::{ AccountId, Aura, Balance, Balances, Block, BlockNumber, Hash, Nonce, PalletInfo, Runtime, @@ -127,7 +130,7 @@ impl pallet_balances::Config for Runtime { /// The ubiquitous event type. type RuntimeEvent = RuntimeEvent; type DustRemoval = (); - type ExistentialDeposit = ConstU128; + type ExistentialDeposit = ConstU128<{EXISTENTIAL_DEPOSIT}>; type AccountStore = System; type WeightInfo = pallet_balances::weights::SubstrateWeight; type FreezeIdentifier = RuntimeFreezeReason; @@ -137,6 +140,8 @@ impl pallet_balances::Config for Runtime { type DoneSlashHandler = (); } + + parameter_types! { pub FeeMultiplier: Multiplier = Multiplier::one(); } @@ -176,10 +181,11 @@ parameter_types! { pub const MaxUserOrders: u32 = 1000; // Max 100 orders per user in tests } -impl pallet_orderbook::Config for Test { +impl pallet_orderbook::Config for Runtime { type RuntimeEvent = RuntimeEvent; type MaxPendingOrders = MaxPendingOrders; type MaxCancellationOrders = MaxCancellationOrders; type MaxOrders = MaxOrders; type MaxUserOrders = MaxUserOrders; } + diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 71f0e9f..393cc47 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -27,6 +27,9 @@ pub use sp_runtime::BuildStorage; pub mod genesis_config_presets; + + + /// Opaque types. These are used by the CLI to instantiate machinery that don't need to know /// the specifics of the runtime. They can then be made to be agnostic over specific formats /// of data like extrinsics, allowing for them to continue syncing the network through upgrades @@ -231,4 +234,4 @@ mod runtime { #[runtime::pallet_index(9)] pub type Orderbook = pallet_orderbook; -} +} \ No newline at end of file From 2ab36c05245dbcc32fcbf1dce688fa225f4f9ebc Mon Sep 17 00:00:00 2001 From: randalllionelkharkrang Date: Thu, 16 Oct 2025 12:06:04 +0530 Subject: [PATCH 07/15] Added benchmarks for orderbook dex, along with cargo fmt. build successfull --- .gitignore | 18 + node/build.rs | 4 +- node/src/benchmarking.rs | 233 +++++----- node/src/chain_spec.rs | 36 +- node/src/cli.rs | 52 +-- node/src/command.rs | 339 ++++++++------- node/src/main.rs | 2 +- node/src/rpc.rs | 58 +-- node/src/service.rs | 595 +++++++++++++------------- pallets/assets/src/benchmarking.rs | 42 +- pallets/assets/src/lib.rs | 167 ++++---- pallets/assets/src/mock.rs | 8 +- pallets/assets/src/tests.rs | 43 +- pallets/orderbook/src/benchmarking.rs | 263 +++++++++++- pallets/orderbook/src/engine.rs | 159 +++---- pallets/orderbook/src/lib.rs | 572 +++++++++++++------------ pallets/orderbook/src/mock.rs | 14 +- pallets/orderbook/src/tests.rs | 321 +++++++------- pallets/orderbook/src/types.rs | 48 ++- pallets/orderbook/src/weights.rs | 550 ++++++++++++++++++++++++ pallets/template/src/benchmarking.rs | 36 +- pallets/template/src/lib.rs | 272 ++++++------ pallets/template/src/mock.rs | 51 +-- pallets/template/src/tests.rs | 37 +- runtime/build.rs | 8 +- runtime/src/apis.rs | 522 +++++++++++----------- runtime/src/benchmarks.rs | 17 +- runtime/src/configs/mod.rs | 185 ++++---- runtime/src/genesis_config_presets.rs | 132 +++--- runtime/src/lib.rs | 228 +++++----- 30 files changed, 2992 insertions(+), 2020 deletions(-) create mode 100644 pallets/orderbook/src/weights.rs diff --git a/.gitignore b/.gitignore index 2f7896d..05a7bb6 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,19 @@ +# macOS +.DS_Store +.AppleDouble +.LSOverride + +# macOS Thumbnails +._* + +# macOS Directories +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + + target/ diff --git a/node/build.rs b/node/build.rs index e3bfe31..f9d839f 100644 --- a/node/build.rs +++ b/node/build.rs @@ -1,7 +1,7 @@ use substrate_build_script_utils::{generate_cargo_keys, rerun_if_git_head_changed}; fn main() { - generate_cargo_keys(); + generate_cargo_keys(); - rerun_if_git_head_changed(); + rerun_if_git_head_changed(); } diff --git a/node/src/benchmarking.rs b/node/src/benchmarking.rs index 467cad4..2fb7726 100644 --- a/node/src/benchmarking.rs +++ b/node/src/benchmarking.rs @@ -19,147 +19,158 @@ use std::{sync::Arc, time::Duration}; /// /// Note: Should only be used for benchmarking. pub struct RemarkBuilder { - client: Arc, + client: Arc, } impl RemarkBuilder { - /// Creates a new [`Self`] from the given client. - pub fn new(client: Arc) -> Self { - Self { client } - } + /// Creates a new [`Self`] from the given client. + pub fn new(client: Arc) -> Self { + Self { client } + } } impl frame_benchmarking_cli::ExtrinsicBuilder for RemarkBuilder { - fn pallet(&self) -> &str { - "system" - } - - fn extrinsic(&self) -> &str { - "remark" - } - - fn build(&self, nonce: u32) -> std::result::Result { - let acc = Sr25519Keyring::Bob.pair(); - let extrinsic: OpaqueExtrinsic = create_benchmark_extrinsic( - self.client.as_ref(), - acc, - SystemCall::remark { remark: vec![] }.into(), - nonce, - ) - .into(); - - Ok(extrinsic) - } + fn pallet(&self) -> &str { + "system" + } + + fn extrinsic(&self) -> &str { + "remark" + } + + fn build(&self, nonce: u32) -> std::result::Result { + let acc = Sr25519Keyring::Bob.pair(); + let extrinsic: OpaqueExtrinsic = create_benchmark_extrinsic( + self.client.as_ref(), + acc, + SystemCall::remark { remark: vec![] }.into(), + nonce, + ) + .into(); + + Ok(extrinsic) + } } /// Generates `Balances::TransferKeepAlive` extrinsics for the benchmarks. /// /// Note: Should only be used for benchmarking. pub struct TransferKeepAliveBuilder { - client: Arc, - dest: AccountId, - value: Balance, + client: Arc, + dest: AccountId, + value: Balance, } impl TransferKeepAliveBuilder { - /// Creates a new [`Self`] from the given client. - pub fn new(client: Arc, dest: AccountId, value: Balance) -> Self { - Self { client, dest, value } - } + /// Creates a new [`Self`] from the given client. + pub fn new(client: Arc, dest: AccountId, value: Balance) -> Self { + Self { + client, + dest, + value, + } + } } impl frame_benchmarking_cli::ExtrinsicBuilder for TransferKeepAliveBuilder { - fn pallet(&self) -> &str { - "balances" - } - - fn extrinsic(&self) -> &str { - "transfer_keep_alive" - } - - fn build(&self, nonce: u32) -> std::result::Result { - let acc = Sr25519Keyring::Bob.pair(); - let extrinsic: OpaqueExtrinsic = create_benchmark_extrinsic( - self.client.as_ref(), - acc, - BalancesCall::transfer_keep_alive { dest: self.dest.clone().into(), value: self.value } - .into(), - nonce, - ) - .into(); - - Ok(extrinsic) - } + fn pallet(&self) -> &str { + "balances" + } + + fn extrinsic(&self) -> &str { + "transfer_keep_alive" + } + + fn build(&self, nonce: u32) -> std::result::Result { + let acc = Sr25519Keyring::Bob.pair(); + let extrinsic: OpaqueExtrinsic = create_benchmark_extrinsic( + self.client.as_ref(), + acc, + BalancesCall::transfer_keep_alive { + dest: self.dest.clone().into(), + value: self.value, + } + .into(), + nonce, + ) + .into(); + + Ok(extrinsic) + } } /// Create a transaction using the given `call`. /// /// Note: Should only be used for benchmarking. pub fn create_benchmark_extrinsic( - client: &FullClient, - sender: sp_core::sr25519::Pair, - call: runtime::RuntimeCall, - nonce: u32, + client: &FullClient, + sender: sp_core::sr25519::Pair, + call: runtime::RuntimeCall, + nonce: u32, ) -> runtime::UncheckedExtrinsic { - let genesis_hash = client.block_hash(0).ok().flatten().expect("Genesis block exists; qed"); - let best_hash = client.chain_info().best_hash; - let best_block = client.chain_info().best_number; - - let period = runtime::configs::BlockHashCount::get() - .checked_next_power_of_two() - .map(|c| c / 2) - .unwrap_or(2) as u64; - let tx_ext: runtime::TxExtension = ( - frame_system::CheckNonZeroSender::::new(), - frame_system::CheckSpecVersion::::new(), - frame_system::CheckTxVersion::::new(), - frame_system::CheckGenesis::::new(), - frame_system::CheckEra::::from(sp_runtime::generic::Era::mortal( - period, - best_block.saturated_into(), - )), - frame_system::CheckNonce::::from(nonce), - frame_system::CheckWeight::::new(), - pallet_transaction_payment::ChargeTransactionPayment::::from(0), - frame_metadata_hash_extension::CheckMetadataHash::::new(false), - frame_system::WeightReclaim::::new(), - ); - - let raw_payload = runtime::SignedPayload::from_raw( - call.clone(), - tx_ext.clone(), - ( - (), - runtime::VERSION.spec_version, - runtime::VERSION.transaction_version, - genesis_hash, - best_hash, - (), - (), - (), - None, - (), - ), - ); - let signature = raw_payload.using_encoded(|e| sender.sign(e)); - - runtime::UncheckedExtrinsic::new_signed( - call, - sp_runtime::AccountId32::from(sender.public()).into(), - runtime::Signature::Sr25519(signature), - tx_ext, - ) + let genesis_hash = client + .block_hash(0) + .ok() + .flatten() + .expect("Genesis block exists; qed"); + let best_hash = client.chain_info().best_hash; + let best_block = client.chain_info().best_number; + + let period = runtime::configs::BlockHashCount::get() + .checked_next_power_of_two() + .map(|c| c / 2) + .unwrap_or(2) as u64; + let tx_ext: runtime::TxExtension = ( + frame_system::CheckNonZeroSender::::new(), + frame_system::CheckSpecVersion::::new(), + frame_system::CheckTxVersion::::new(), + frame_system::CheckGenesis::::new(), + frame_system::CheckEra::::from(sp_runtime::generic::Era::mortal( + period, + best_block.saturated_into(), + )), + frame_system::CheckNonce::::from(nonce), + frame_system::CheckWeight::::new(), + pallet_transaction_payment::ChargeTransactionPayment::::from(0), + frame_metadata_hash_extension::CheckMetadataHash::::new(false), + frame_system::WeightReclaim::::new(), + ); + + let raw_payload = runtime::SignedPayload::from_raw( + call.clone(), + tx_ext.clone(), + ( + (), + runtime::VERSION.spec_version, + runtime::VERSION.transaction_version, + genesis_hash, + best_hash, + (), + (), + (), + None, + (), + ), + ); + let signature = raw_payload.using_encoded(|e| sender.sign(e)); + + runtime::UncheckedExtrinsic::new_signed( + call, + sp_runtime::AccountId32::from(sender.public()).into(), + runtime::Signature::Sr25519(signature), + tx_ext, + ) } /// Generates inherent data for the `benchmark overhead` command. /// /// Note: Should only be used for benchmarking. pub fn inherent_benchmark_data() -> Result { - let mut inherent_data = InherentData::new(); - let d = Duration::from_millis(0); - let timestamp = sp_timestamp::InherentDataProvider::new(d.into()); + let mut inherent_data = InherentData::new(); + let d = Duration::from_millis(0); + let timestamp = sp_timestamp::InherentDataProvider::new(d.into()); - futures::executor::block_on(timestamp.provide_inherent_data(&mut inherent_data)) - .map_err(|e| format!("creating inherent data: {:?}", e))?; - Ok(inherent_data) + futures::executor::block_on(timestamp.provide_inherent_data(&mut inherent_data)) + .map_err(|e| format!("creating inherent data: {:?}", e))?; + Ok(inherent_data) } diff --git a/node/src/chain_spec.rs b/node/src/chain_spec.rs index 086bf7a..22640cd 100644 --- a/node/src/chain_spec.rs +++ b/node/src/chain_spec.rs @@ -5,25 +5,25 @@ use solochain_template_runtime::WASM_BINARY; pub type ChainSpec = sc_service::GenericChainSpec; pub fn development_chain_spec() -> Result { - Ok(ChainSpec::builder( - WASM_BINARY.ok_or_else(|| "Development wasm not available".to_string())?, - None, - ) - .with_name("Development") - .with_id("dev") - .with_chain_type(ChainType::Development) - .with_genesis_config_preset_name(sp_genesis_builder::DEV_RUNTIME_PRESET) - .build()) + Ok(ChainSpec::builder( + WASM_BINARY.ok_or_else(|| "Development wasm not available".to_string())?, + None, + ) + .with_name("Development") + .with_id("dev") + .with_chain_type(ChainType::Development) + .with_genesis_config_preset_name(sp_genesis_builder::DEV_RUNTIME_PRESET) + .build()) } pub fn local_chain_spec() -> Result { - Ok(ChainSpec::builder( - WASM_BINARY.ok_or_else(|| "Development wasm not available".to_string())?, - None, - ) - .with_name("Local Testnet") - .with_id("local_testnet") - .with_chain_type(ChainType::Local) - .with_genesis_config_preset_name(sp_genesis_builder::LOCAL_TESTNET_RUNTIME_PRESET) - .build()) + Ok(ChainSpec::builder( + WASM_BINARY.ok_or_else(|| "Development wasm not available".to_string())?, + None, + ) + .with_name("Local Testnet") + .with_id("local_testnet") + .with_chain_type(ChainType::Local) + .with_genesis_config_preset_name(sp_genesis_builder::LOCAL_TESTNET_RUNTIME_PRESET) + .build()) } diff --git a/node/src/cli.rs b/node/src/cli.rs index b2c53aa..72d4465 100644 --- a/node/src/cli.rs +++ b/node/src/cli.rs @@ -2,45 +2,45 @@ use sc_cli::RunCmd; #[derive(Debug, clap::Parser)] pub struct Cli { - #[command(subcommand)] - pub subcommand: Option, + #[command(subcommand)] + pub subcommand: Option, - #[clap(flatten)] - pub run: RunCmd, + #[clap(flatten)] + pub run: RunCmd, } #[derive(Debug, clap::Subcommand)] #[allow(clippy::large_enum_variant)] pub enum Subcommand { - /// Key management cli utilities - #[command(subcommand)] - Key(sc_cli::KeySubcommand), + /// Key management cli utilities + #[command(subcommand)] + Key(sc_cli::KeySubcommand), - /// Build a chain specification. - BuildSpec(sc_cli::BuildSpecCmd), + /// Build a chain specification. + BuildSpec(sc_cli::BuildSpecCmd), - /// Validate blocks. - CheckBlock(sc_cli::CheckBlockCmd), + /// Validate blocks. + CheckBlock(sc_cli::CheckBlockCmd), - /// Export blocks. - ExportBlocks(sc_cli::ExportBlocksCmd), + /// Export blocks. + ExportBlocks(sc_cli::ExportBlocksCmd), - /// Export the state of a given block into a chain spec. - ExportState(sc_cli::ExportStateCmd), + /// Export the state of a given block into a chain spec. + ExportState(sc_cli::ExportStateCmd), - /// Import blocks. - ImportBlocks(sc_cli::ImportBlocksCmd), + /// Import blocks. + ImportBlocks(sc_cli::ImportBlocksCmd), - /// Remove the whole chain. - PurgeChain(sc_cli::PurgeChainCmd), + /// Remove the whole chain. + PurgeChain(sc_cli::PurgeChainCmd), - /// Revert the chain to a previous state. - Revert(sc_cli::RevertCmd), + /// Revert the chain to a previous state. + Revert(sc_cli::RevertCmd), - /// Sub-commands concerned with benchmarking. - #[command(subcommand)] - Benchmark(frame_benchmarking_cli::BenchmarkCmd), + /// Sub-commands concerned with benchmarking. + #[command(subcommand)] + Benchmark(frame_benchmarking_cli::BenchmarkCmd), - /// Db meta columns information. - ChainInfo(sc_cli::ChainInfoCmd), + /// Db meta columns information. + ChainInfo(sc_cli::ChainInfoCmd), } diff --git a/node/src/command.rs b/node/src/command.rs index 54540db..bdebad1 100644 --- a/node/src/command.rs +++ b/node/src/command.rs @@ -1,8 +1,8 @@ use crate::{ - benchmarking::{inherent_benchmark_data, RemarkBuilder, TransferKeepAliveBuilder}, - chain_spec, - cli::{Cli, Subcommand}, - service, + benchmarking::{inherent_benchmark_data, RemarkBuilder, TransferKeepAliveBuilder}, + chain_spec, + cli::{Cli, Subcommand}, + service, }; use frame_benchmarking_cli::{BenchmarkCmd, ExtrinsicFactory, SUBSTRATE_REFERENCE_HARDWARE}; use sc_cli::SubstrateCli; @@ -11,174 +11,197 @@ use solochain_template_runtime::{Block, EXISTENTIAL_DEPOSIT}; use sp_keyring::Sr25519Keyring; impl SubstrateCli for Cli { - fn impl_name() -> String { - "Substrate Node".into() - } + fn impl_name() -> String { + "Substrate Node".into() + } - fn impl_version() -> String { - env!("SUBSTRATE_CLI_IMPL_VERSION").into() - } + fn impl_version() -> String { + env!("SUBSTRATE_CLI_IMPL_VERSION").into() + } - fn description() -> String { - env!("CARGO_PKG_DESCRIPTION").into() - } + fn description() -> String { + env!("CARGO_PKG_DESCRIPTION").into() + } - fn author() -> String { - env!("CARGO_PKG_AUTHORS").into() - } + fn author() -> String { + env!("CARGO_PKG_AUTHORS").into() + } - fn support_url() -> String { - "support.anonymous.an".into() - } + fn support_url() -> String { + "support.anonymous.an".into() + } - fn copyright_start_year() -> i32 { - 2017 - } + fn copyright_start_year() -> i32 { + 2017 + } - fn load_spec(&self, id: &str) -> Result, String> { - Ok(match id { - "dev" => Box::new(chain_spec::development_chain_spec()?), - "" | "local" => Box::new(chain_spec::local_chain_spec()?), - path => - Box::new(chain_spec::ChainSpec::from_json_file(std::path::PathBuf::from(path))?), - }) - } + fn load_spec(&self, id: &str) -> Result, String> { + Ok(match id { + "dev" => Box::new(chain_spec::development_chain_spec()?), + "" | "local" => Box::new(chain_spec::local_chain_spec()?), + path => Box::new(chain_spec::ChainSpec::from_json_file( + std::path::PathBuf::from(path), + )?), + }) + } } /// Parse and run command line arguments pub fn run() -> sc_cli::Result<()> { - let cli = Cli::from_args(); + let cli = Cli::from_args(); - match &cli.subcommand { - Some(Subcommand::Key(cmd)) => cmd.run(&cli), - Some(Subcommand::BuildSpec(cmd)) => { - let runner = cli.create_runner(cmd)?; - runner.sync_run(|config| cmd.run(config.chain_spec, config.network)) - }, - Some(Subcommand::CheckBlock(cmd)) => { - let runner = cli.create_runner(cmd)?; - runner.async_run(|config| { - let PartialComponents { client, task_manager, import_queue, .. } = - service::new_partial(&config)?; - Ok((cmd.run(client, import_queue), task_manager)) - }) - }, - Some(Subcommand::ExportBlocks(cmd)) => { - let runner = cli.create_runner(cmd)?; - runner.async_run(|config| { - let PartialComponents { client, task_manager, .. } = service::new_partial(&config)?; - Ok((cmd.run(client, config.database), task_manager)) - }) - }, - Some(Subcommand::ExportState(cmd)) => { - let runner = cli.create_runner(cmd)?; - runner.async_run(|config| { - let PartialComponents { client, task_manager, .. } = service::new_partial(&config)?; - Ok((cmd.run(client, config.chain_spec), task_manager)) - }) - }, - Some(Subcommand::ImportBlocks(cmd)) => { - let runner = cli.create_runner(cmd)?; - runner.async_run(|config| { - let PartialComponents { client, task_manager, import_queue, .. } = - service::new_partial(&config)?; - Ok((cmd.run(client, import_queue), task_manager)) - }) - }, - Some(Subcommand::PurgeChain(cmd)) => { - let runner = cli.create_runner(cmd)?; - runner.sync_run(|config| cmd.run(config.database)) - }, - Some(Subcommand::Revert(cmd)) => { - let runner = cli.create_runner(cmd)?; - runner.async_run(|config| { - let PartialComponents { client, task_manager, backend, .. } = - service::new_partial(&config)?; - let aux_revert = Box::new(|client, _, blocks| { - sc_consensus_grandpa::revert(client, blocks)?; - Ok(()) - }); - Ok((cmd.run(client, backend, Some(aux_revert)), task_manager)) - }) - }, - Some(Subcommand::Benchmark(cmd)) => { - let runner = cli.create_runner(cmd)?; + match &cli.subcommand { + Some(Subcommand::Key(cmd)) => cmd.run(&cli), + Some(Subcommand::BuildSpec(cmd)) => { + let runner = cli.create_runner(cmd)?; + runner.sync_run(|config| cmd.run(config.chain_spec, config.network)) + } + Some(Subcommand::CheckBlock(cmd)) => { + let runner = cli.create_runner(cmd)?; + runner.async_run(|config| { + let PartialComponents { + client, + task_manager, + import_queue, + .. + } = service::new_partial(&config)?; + Ok((cmd.run(client, import_queue), task_manager)) + }) + } + Some(Subcommand::ExportBlocks(cmd)) => { + let runner = cli.create_runner(cmd)?; + runner.async_run(|config| { + let PartialComponents { + client, + task_manager, + .. + } = service::new_partial(&config)?; + Ok((cmd.run(client, config.database), task_manager)) + }) + } + Some(Subcommand::ExportState(cmd)) => { + let runner = cli.create_runner(cmd)?; + runner.async_run(|config| { + let PartialComponents { + client, + task_manager, + .. + } = service::new_partial(&config)?; + Ok((cmd.run(client, config.chain_spec), task_manager)) + }) + } + Some(Subcommand::ImportBlocks(cmd)) => { + let runner = cli.create_runner(cmd)?; + runner.async_run(|config| { + let PartialComponents { + client, + task_manager, + import_queue, + .. + } = service::new_partial(&config)?; + Ok((cmd.run(client, import_queue), task_manager)) + }) + } + Some(Subcommand::PurgeChain(cmd)) => { + let runner = cli.create_runner(cmd)?; + runner.sync_run(|config| cmd.run(config.database)) + } + Some(Subcommand::Revert(cmd)) => { + let runner = cli.create_runner(cmd)?; + runner.async_run(|config| { + let PartialComponents { + client, + task_manager, + backend, + .. + } = service::new_partial(&config)?; + let aux_revert = Box::new(|client, _, blocks| { + sc_consensus_grandpa::revert(client, blocks)?; + Ok(()) + }); + Ok((cmd.run(client, backend, Some(aux_revert)), task_manager)) + }) + } + Some(Subcommand::Benchmark(cmd)) => { + let runner = cli.create_runner(cmd)?; - runner.sync_run(|config| { - // This switch needs to be in the client, since the client decides - // which sub-commands it wants to support. - match cmd { - BenchmarkCmd::Pallet(cmd) => { - if !cfg!(feature = "runtime-benchmarks") { - return Err( - "Runtime benchmarking wasn't enabled when building the node. \ + runner.sync_run(|config| { + // This switch needs to be in the client, since the client decides + // which sub-commands it wants to support. + match cmd { + BenchmarkCmd::Pallet(cmd) => { + if !cfg!(feature = "runtime-benchmarks") { + return Err( + "Runtime benchmarking wasn't enabled when building the node. \ You can enable it with `--features runtime-benchmarks`." - .into(), - ); - } + .into(), + ); + } - cmd.run_with_spec::, ()>(Some( - config.chain_spec, - )) - }, - BenchmarkCmd::Block(cmd) => { - let PartialComponents { client, .. } = service::new_partial(&config)?; - cmd.run(client) - }, - #[cfg(not(feature = "runtime-benchmarks"))] - BenchmarkCmd::Storage(_) => Err( - "Storage benchmarking can be enabled with `--features runtime-benchmarks`." - .into(), - ), - #[cfg(feature = "runtime-benchmarks")] - BenchmarkCmd::Storage(cmd) => { - let PartialComponents { client, backend, .. } = - service::new_partial(&config)?; - let db = backend.expose_db(); - let storage = backend.expose_storage(); + cmd.run_with_spec::, ()>(Some( + config.chain_spec, + )) + } + BenchmarkCmd::Block(cmd) => { + let PartialComponents { client, .. } = service::new_partial(&config)?; + cmd.run(client) + } + #[cfg(not(feature = "runtime-benchmarks"))] + BenchmarkCmd::Storage(_) => Err( + "Storage benchmarking can be enabled with `--features runtime-benchmarks`." + .into(), + ), + #[cfg(feature = "runtime-benchmarks")] + BenchmarkCmd::Storage(cmd) => { + let PartialComponents { + client, backend, .. + } = service::new_partial(&config)?; + let db = backend.expose_db(); + let storage = backend.expose_storage(); - cmd.run(config, client, db, storage) - }, - BenchmarkCmd::Overhead(cmd) => { - let PartialComponents { client, .. } = service::new_partial(&config)?; - let ext_builder = RemarkBuilder::new(client.clone()); + cmd.run(config, client, db, storage) + } + BenchmarkCmd::Overhead(cmd) => { + let PartialComponents { client, .. } = service::new_partial(&config)?; + let ext_builder = RemarkBuilder::new(client.clone()); - cmd.run( - config.chain_spec.name().into(), - client, - inherent_benchmark_data()?, - Vec::new(), - &ext_builder, - false, - ) - }, - BenchmarkCmd::Extrinsic(cmd) => { - let PartialComponents { client, .. } = service::new_partial(&config)?; - // Register the *Remark* and *TKA* builders. - let ext_factory = ExtrinsicFactory(vec![ - Box::new(RemarkBuilder::new(client.clone())), - Box::new(TransferKeepAliveBuilder::new( - client.clone(), - Sr25519Keyring::Alice.to_account_id(), - EXISTENTIAL_DEPOSIT, - )), - ]); + cmd.run( + config.chain_spec.name().into(), + client, + inherent_benchmark_data()?, + Vec::new(), + &ext_builder, + false, + ) + } + BenchmarkCmd::Extrinsic(cmd) => { + let PartialComponents { client, .. } = service::new_partial(&config)?; + // Register the *Remark* and *TKA* builders. + let ext_factory = ExtrinsicFactory(vec![ + Box::new(RemarkBuilder::new(client.clone())), + Box::new(TransferKeepAliveBuilder::new( + client.clone(), + Sr25519Keyring::Alice.to_account_id(), + EXISTENTIAL_DEPOSIT, + )), + ]); - cmd.run(client, inherent_benchmark_data()?, Vec::new(), &ext_factory) - }, - BenchmarkCmd::Machine(cmd) => - cmd.run(&config, SUBSTRATE_REFERENCE_HARDWARE.clone()), - } - }) - }, - Some(Subcommand::ChainInfo(cmd)) => { - let runner = cli.create_runner(cmd)?; - runner.sync_run(|config| cmd.run::(&config)) - }, - None => { - let runner = cli.create_runner(&cli.run)?; - runner.run_node_until_exit(|config| async move { - match config.network.network_backend.unwrap_or_default() { + cmd.run(client, inherent_benchmark_data()?, Vec::new(), &ext_factory) + } + BenchmarkCmd::Machine(cmd) => { + cmd.run(&config, SUBSTRATE_REFERENCE_HARDWARE.clone()) + } + } + }) + } + Some(Subcommand::ChainInfo(cmd)) => { + let runner = cli.create_runner(cmd)?; + runner.sync_run(|config| cmd.run::(&config)) + } + None => { + let runner = cli.create_runner(&cli.run)?; + runner.run_node_until_exit(|config| async move { + match config.network.network_backend.unwrap_or_default() { sc_network::config::NetworkBackendType::Libp2p => service::new_full::< sc_network::NetworkWorker< solochain_template_runtime::opaque::Block, @@ -190,7 +213,7 @@ pub fn run() -> sc_cli::Result<()> { service::new_full::(config) .map_err(sc_cli::Error::Service), } - }) - }, - } + }) + } + } } diff --git a/node/src/main.rs b/node/src/main.rs index 8918dd4..2149818 100644 --- a/node/src/main.rs +++ b/node/src/main.rs @@ -9,5 +9,5 @@ mod rpc; mod service; fn main() -> sc_cli::Result<()> { - command::run() + command::run() } diff --git a/node/src/rpc.rs b/node/src/rpc.rs index 1fc6eb0..a31fdf4 100644 --- a/node/src/rpc.rs +++ b/node/src/rpc.rs @@ -16,45 +16,45 @@ use sp_blockchain::{Error as BlockChainError, HeaderBackend, HeaderMetadata}; /// Full client dependencies. pub struct FullDeps { - /// The client instance to use. - pub client: Arc, - /// Transaction pool instance. - pub pool: Arc

, + /// The client instance to use. + pub client: Arc, + /// Transaction pool instance. + pub pool: Arc

, } /// Instantiate all full RPC extensions. pub fn create_full( - deps: FullDeps, + deps: FullDeps, ) -> Result, Box> where - C: ProvideRuntimeApi, - C: HeaderBackend + HeaderMetadata + 'static, - C: Send + Sync + 'static, - C::Api: substrate_frame_rpc_system::AccountNonceApi, - C::Api: pallet_transaction_payment_rpc::TransactionPaymentRuntimeApi, - C::Api: BlockBuilder, - P: TransactionPool + 'static, + C: ProvideRuntimeApi, + C: HeaderBackend + HeaderMetadata + 'static, + C: Send + Sync + 'static, + C::Api: substrate_frame_rpc_system::AccountNonceApi, + C::Api: pallet_transaction_payment_rpc::TransactionPaymentRuntimeApi, + C::Api: BlockBuilder, + P: TransactionPool + 'static, { - use pallet_transaction_payment_rpc::{TransactionPayment, TransactionPaymentApiServer}; - use substrate_frame_rpc_system::{System, SystemApiServer}; + use pallet_transaction_payment_rpc::{TransactionPayment, TransactionPaymentApiServer}; + use substrate_frame_rpc_system::{System, SystemApiServer}; - let mut module = RpcModule::new(()); - let FullDeps { client, pool } = deps; + let mut module = RpcModule::new(()); + let FullDeps { client, pool } = deps; - module.merge(System::new(client.clone(), pool).into_rpc())?; - module.merge(TransactionPayment::new(client).into_rpc())?; + module.merge(System::new(client.clone(), pool).into_rpc())?; + module.merge(TransactionPayment::new(client).into_rpc())?; - // Extend this RPC with a custom API by using the following syntax. - // `YourRpcStruct` should have a reference to a client, which is needed - // to call into the runtime. - // `module.merge(YourRpcTrait::into_rpc(YourRpcStruct::new(ReferenceToClient, ...)))?;` + // Extend this RPC with a custom API by using the following syntax. + // `YourRpcStruct` should have a reference to a client, which is needed + // to call into the runtime. + // `module.merge(YourRpcTrait::into_rpc(YourRpcStruct::new(ReferenceToClient, ...)))?;` - // You probably want to enable the `rpc v2 chainSpec` API as well - // - // let chain_name = chain_spec.name().to_string(); - // let genesis_hash = client.block_hash(0).ok().flatten().expect("Genesis block exists; qed"); - // let properties = chain_spec.properties(); - // module.merge(ChainSpec::new(chain_name, genesis_hash, properties).into_rpc())?; + // You probably want to enable the `rpc v2 chainSpec` API as well + // + // let chain_name = chain_spec.name().to_string(); + // let genesis_hash = client.block_hash(0).ok().flatten().expect("Genesis block exists; qed"); + // let properties = chain_spec.properties(); + // module.merge(ChainSpec::new(chain_name, genesis_hash, properties).into_rpc())?; - Ok(module) + Ok(module) } diff --git a/node/src/service.rs b/node/src/service.rs index 79d97fb..c49423b 100644 --- a/node/src/service.rs +++ b/node/src/service.rs @@ -12,9 +12,9 @@ use sp_consensus_aura::sr25519::AuthorityPair as AuraPair; use std::{sync::Arc, time::Duration}; pub(crate) type FullClient = sc_service::TFullClient< - Block, - RuntimeApi, - sc_executor::WasmExecutor, + Block, + RuntimeApi, + sc_executor::WasmExecutor, >; type FullBackend = sc_service::TFullBackend; type FullSelectChain = sc_consensus::LongestChain; @@ -24,310 +24,325 @@ type FullSelectChain = sc_consensus::LongestChain; const GRANDPA_JUSTIFICATION_PERIOD: u32 = 512; pub type Service = sc_service::PartialComponents< - FullClient, - FullBackend, - FullSelectChain, - sc_consensus::DefaultImportQueue, - sc_transaction_pool::TransactionPoolHandle, - ( - sc_consensus_grandpa::GrandpaBlockImport, - sc_consensus_grandpa::LinkHalf, - Option, - ), + FullClient, + FullBackend, + FullSelectChain, + sc_consensus::DefaultImportQueue, + sc_transaction_pool::TransactionPoolHandle, + ( + sc_consensus_grandpa::GrandpaBlockImport, + sc_consensus_grandpa::LinkHalf, + Option, + ), >; pub fn new_partial(config: &Configuration) -> Result { - let telemetry = config - .telemetry_endpoints - .clone() - .filter(|x| !x.is_empty()) - .map(|endpoints| -> Result<_, sc_telemetry::Error> { - let worker = TelemetryWorker::new(16)?; - let telemetry = worker.handle().new_telemetry(endpoints); - Ok((worker, telemetry)) - }) - .transpose()?; - - let executor = sc_service::new_wasm_executor::(&config.executor); - let (client, backend, keystore_container, task_manager) = - sc_service::new_full_parts::( - config, - telemetry.as_ref().map(|(_, telemetry)| telemetry.handle()), - executor, - )?; - let client = Arc::new(client); - - let telemetry = telemetry.map(|(worker, telemetry)| { - task_manager.spawn_handle().spawn("telemetry", None, worker.run()); - telemetry - }); - - let select_chain = sc_consensus::LongestChain::new(backend.clone()); - - let transaction_pool = Arc::from( - sc_transaction_pool::Builder::new( - task_manager.spawn_essential_handle(), - client.clone(), - config.role.is_authority().into(), - ) - .with_options(config.transaction_pool.clone()) - .with_prometheus(config.prometheus_registry()) - .build(), - ); - - let (grandpa_block_import, grandpa_link) = sc_consensus_grandpa::block_import( - client.clone(), - GRANDPA_JUSTIFICATION_PERIOD, - &client, - select_chain.clone(), - telemetry.as_ref().map(|x| x.handle()), - )?; - - let cidp_client = client.clone(); - let import_queue = - sc_consensus_aura::import_queue::(ImportQueueParams { - block_import: grandpa_block_import.clone(), - justification_import: Some(Box::new(grandpa_block_import.clone())), - client: client.clone(), - create_inherent_data_providers: move |parent_hash, _| { - let cidp_client = cidp_client.clone(); - async move { - let slot_duration = sc_consensus_aura::standalone::slot_duration_at( - &*cidp_client, - parent_hash, - )?; - let timestamp = sp_timestamp::InherentDataProvider::from_system_time(); - - let slot = + let telemetry = config + .telemetry_endpoints + .clone() + .filter(|x| !x.is_empty()) + .map(|endpoints| -> Result<_, sc_telemetry::Error> { + let worker = TelemetryWorker::new(16)?; + let telemetry = worker.handle().new_telemetry(endpoints); + Ok((worker, telemetry)) + }) + .transpose()?; + + let executor = sc_service::new_wasm_executor::(&config.executor); + let (client, backend, keystore_container, task_manager) = + sc_service::new_full_parts::( + config, + telemetry.as_ref().map(|(_, telemetry)| telemetry.handle()), + executor, + )?; + let client = Arc::new(client); + + let telemetry = telemetry.map(|(worker, telemetry)| { + task_manager + .spawn_handle() + .spawn("telemetry", None, worker.run()); + telemetry + }); + + let select_chain = sc_consensus::LongestChain::new(backend.clone()); + + let transaction_pool = Arc::from( + sc_transaction_pool::Builder::new( + task_manager.spawn_essential_handle(), + client.clone(), + config.role.is_authority().into(), + ) + .with_options(config.transaction_pool.clone()) + .with_prometheus(config.prometheus_registry()) + .build(), + ); + + let (grandpa_block_import, grandpa_link) = sc_consensus_grandpa::block_import( + client.clone(), + GRANDPA_JUSTIFICATION_PERIOD, + &client, + select_chain.clone(), + telemetry.as_ref().map(|x| x.handle()), + )?; + + let cidp_client = client.clone(); + let import_queue = + sc_consensus_aura::import_queue::(ImportQueueParams { + block_import: grandpa_block_import.clone(), + justification_import: Some(Box::new(grandpa_block_import.clone())), + client: client.clone(), + create_inherent_data_providers: move |parent_hash, _| { + let cidp_client = cidp_client.clone(); + async move { + let slot_duration = sc_consensus_aura::standalone::slot_duration_at( + &*cidp_client, + parent_hash, + )?; + let timestamp = sp_timestamp::InherentDataProvider::from_system_time(); + + let slot = sp_consensus_aura::inherents::InherentDataProvider::from_timestamp_and_slot_duration( *timestamp, slot_duration, ); - Ok((slot, timestamp)) - } - }, - spawner: &task_manager.spawn_essential_handle(), - registry: config.prometheus_registry(), - check_for_equivocation: Default::default(), - telemetry: telemetry.as_ref().map(|x| x.handle()), - compatibility_mode: Default::default(), - })?; - - Ok(sc_service::PartialComponents { - client, - backend, - task_manager, - import_queue, - keystore_container, - select_chain, - transaction_pool, - other: (grandpa_block_import, grandpa_link, telemetry), - }) + Ok((slot, timestamp)) + } + }, + spawner: &task_manager.spawn_essential_handle(), + registry: config.prometheus_registry(), + check_for_equivocation: Default::default(), + telemetry: telemetry.as_ref().map(|x| x.handle()), + compatibility_mode: Default::default(), + })?; + + Ok(sc_service::PartialComponents { + client, + backend, + task_manager, + import_queue, + keystore_container, + select_chain, + transaction_pool, + other: (grandpa_block_import, grandpa_link, telemetry), + }) } /// Builds a new service for a full client. pub fn new_full< - N: sc_network::NetworkBackend::Hash>, + N: sc_network::NetworkBackend::Hash>, >( - config: Configuration, + config: Configuration, ) -> Result { - let sc_service::PartialComponents { - client, - backend, - mut task_manager, - import_queue, - keystore_container, - select_chain, - transaction_pool, - other: (block_import, grandpa_link, mut telemetry), - } = new_partial(&config)?; - - let mut net_config = sc_network::config::FullNetworkConfiguration::< - Block, - ::Hash, - N, - >::new(&config.network, config.prometheus_registry().cloned()); - let metrics = N::register_notification_metrics(config.prometheus_registry()); - - let peer_store_handle = net_config.peer_store_handle(); - let grandpa_protocol_name = sc_consensus_grandpa::protocol_standard_name( - &client.block_hash(0).ok().flatten().expect("Genesis block exists; qed"), - &config.chain_spec, - ); - let (grandpa_protocol_config, grandpa_notification_service) = - sc_consensus_grandpa::grandpa_peers_set_config::<_, N>( - grandpa_protocol_name.clone(), - metrics.clone(), - peer_store_handle, - ); - net_config.add_notification_protocol(grandpa_protocol_config); - - let warp_sync = Arc::new(sc_consensus_grandpa::warp_proof::NetworkProvider::new( - backend.clone(), - grandpa_link.shared_authority_set().clone(), - Vec::default(), - )); - - let (network, system_rpc_tx, tx_handler_controller, sync_service) = - sc_service::build_network(sc_service::BuildNetworkParams { - config: &config, - net_config, - client: client.clone(), - transaction_pool: transaction_pool.clone(), - spawn_handle: task_manager.spawn_handle(), - import_queue, - block_announce_validator_builder: None, - warp_sync_config: Some(WarpSyncConfig::WithProvider(warp_sync)), - block_relay: None, - metrics, - })?; - - if config.offchain_worker.enabled { - let offchain_workers = - sc_offchain::OffchainWorkers::new(sc_offchain::OffchainWorkerOptions { - runtime_api_provider: client.clone(), - is_validator: config.role.is_authority(), - keystore: Some(keystore_container.keystore()), - offchain_db: backend.offchain_storage(), - transaction_pool: Some(OffchainTransactionPoolFactory::new( - transaction_pool.clone(), - )), - network_provider: Arc::new(network.clone()), - enable_http_requests: true, - custom_extensions: |_| vec![], - })?; - task_manager.spawn_handle().spawn( - "offchain-workers-runner", - "offchain-worker", - offchain_workers.run(client.clone(), task_manager.spawn_handle()).boxed(), - ); - } - - let role = config.role; - let force_authoring = config.force_authoring; - let backoff_authoring_blocks: Option<()> = None; - let name = config.network.node_name.clone(); - let enable_grandpa = !config.disable_grandpa; - let prometheus_registry = config.prometheus_registry().cloned(); - - let rpc_extensions_builder = { - let client = client.clone(); - let pool = transaction_pool.clone(); - - Box::new(move |_| { - let deps = crate::rpc::FullDeps { client: client.clone(), pool: pool.clone() }; - crate::rpc::create_full(deps).map_err(Into::into) - }) - }; - - let _rpc_handlers = sc_service::spawn_tasks(sc_service::SpawnTasksParams { - network: Arc::new(network.clone()), - client: client.clone(), - keystore: keystore_container.keystore(), - task_manager: &mut task_manager, - transaction_pool: transaction_pool.clone(), - rpc_builder: rpc_extensions_builder, - backend, - system_rpc_tx, - tx_handler_controller, - sync_service: sync_service.clone(), - config, - telemetry: telemetry.as_mut(), - })?; - - if role.is_authority() { - let proposer_factory = sc_basic_authorship::ProposerFactory::new( - task_manager.spawn_handle(), - client.clone(), - transaction_pool.clone(), - prometheus_registry.as_ref(), - telemetry.as_ref().map(|x| x.handle()), - ); - - let slot_duration = sc_consensus_aura::slot_duration(&*client)?; - - let aura = sc_consensus_aura::start_aura::( - StartAuraParams { - slot_duration, - client, - select_chain, - block_import, - proposer_factory, - create_inherent_data_providers: move |_, ()| async move { - let timestamp = sp_timestamp::InherentDataProvider::from_system_time(); - - let slot = + let sc_service::PartialComponents { + client, + backend, + mut task_manager, + import_queue, + keystore_container, + select_chain, + transaction_pool, + other: (block_import, grandpa_link, mut telemetry), + } = new_partial(&config)?; + + let mut net_config = sc_network::config::FullNetworkConfiguration::< + Block, + ::Hash, + N, + >::new(&config.network, config.prometheus_registry().cloned()); + let metrics = N::register_notification_metrics(config.prometheus_registry()); + + let peer_store_handle = net_config.peer_store_handle(); + let grandpa_protocol_name = sc_consensus_grandpa::protocol_standard_name( + &client + .block_hash(0) + .ok() + .flatten() + .expect("Genesis block exists; qed"), + &config.chain_spec, + ); + let (grandpa_protocol_config, grandpa_notification_service) = + sc_consensus_grandpa::grandpa_peers_set_config::<_, N>( + grandpa_protocol_name.clone(), + metrics.clone(), + peer_store_handle, + ); + net_config.add_notification_protocol(grandpa_protocol_config); + + let warp_sync = Arc::new(sc_consensus_grandpa::warp_proof::NetworkProvider::new( + backend.clone(), + grandpa_link.shared_authority_set().clone(), + Vec::default(), + )); + + let (network, system_rpc_tx, tx_handler_controller, sync_service) = + sc_service::build_network(sc_service::BuildNetworkParams { + config: &config, + net_config, + client: client.clone(), + transaction_pool: transaction_pool.clone(), + spawn_handle: task_manager.spawn_handle(), + import_queue, + block_announce_validator_builder: None, + warp_sync_config: Some(WarpSyncConfig::WithProvider(warp_sync)), + block_relay: None, + metrics, + })?; + + if config.offchain_worker.enabled { + let offchain_workers = + sc_offchain::OffchainWorkers::new(sc_offchain::OffchainWorkerOptions { + runtime_api_provider: client.clone(), + is_validator: config.role.is_authority(), + keystore: Some(keystore_container.keystore()), + offchain_db: backend.offchain_storage(), + transaction_pool: Some(OffchainTransactionPoolFactory::new( + transaction_pool.clone(), + )), + network_provider: Arc::new(network.clone()), + enable_http_requests: true, + custom_extensions: |_| vec![], + })?; + task_manager.spawn_handle().spawn( + "offchain-workers-runner", + "offchain-worker", + offchain_workers + .run(client.clone(), task_manager.spawn_handle()) + .boxed(), + ); + } + + let role = config.role; + let force_authoring = config.force_authoring; + let backoff_authoring_blocks: Option<()> = None; + let name = config.network.node_name.clone(); + let enable_grandpa = !config.disable_grandpa; + let prometheus_registry = config.prometheus_registry().cloned(); + + let rpc_extensions_builder = { + let client = client.clone(); + let pool = transaction_pool.clone(); + + Box::new(move |_| { + let deps = crate::rpc::FullDeps { + client: client.clone(), + pool: pool.clone(), + }; + crate::rpc::create_full(deps).map_err(Into::into) + }) + }; + + let _rpc_handlers = sc_service::spawn_tasks(sc_service::SpawnTasksParams { + network: Arc::new(network.clone()), + client: client.clone(), + keystore: keystore_container.keystore(), + task_manager: &mut task_manager, + transaction_pool: transaction_pool.clone(), + rpc_builder: rpc_extensions_builder, + backend, + system_rpc_tx, + tx_handler_controller, + sync_service: sync_service.clone(), + config, + telemetry: telemetry.as_mut(), + })?; + + if role.is_authority() { + let proposer_factory = sc_basic_authorship::ProposerFactory::new( + task_manager.spawn_handle(), + client.clone(), + transaction_pool.clone(), + prometheus_registry.as_ref(), + telemetry.as_ref().map(|x| x.handle()), + ); + + let slot_duration = sc_consensus_aura::slot_duration(&*client)?; + + let aura = sc_consensus_aura::start_aura::( + StartAuraParams { + slot_duration, + client, + select_chain, + block_import, + proposer_factory, + create_inherent_data_providers: move |_, ()| async move { + let timestamp = sp_timestamp::InherentDataProvider::from_system_time(); + + let slot = sp_consensus_aura::inherents::InherentDataProvider::from_timestamp_and_slot_duration( *timestamp, slot_duration, ); - Ok((slot, timestamp)) - }, - force_authoring, - backoff_authoring_blocks, - keystore: keystore_container.keystore(), - sync_oracle: sync_service.clone(), - justification_sync_link: sync_service.clone(), - block_proposal_slot_portion: SlotProportion::new(2f32 / 3f32), - max_block_proposal_slot_portion: None, - telemetry: telemetry.as_ref().map(|x| x.handle()), - compatibility_mode: Default::default(), - }, - )?; - - // the AURA authoring task is considered essential, i.e. if it - // fails we take down the service with it. - task_manager - .spawn_essential_handle() - .spawn_blocking("aura", Some("block-authoring"), aura); - } - - if enable_grandpa { - // if the node isn't actively participating in consensus then it doesn't - // need a keystore, regardless of which protocol we use below. - let keystore = if role.is_authority() { Some(keystore_container.keystore()) } else { None }; - - let grandpa_config = sc_consensus_grandpa::Config { - // FIXME #1578 make this available through chainspec - gossip_duration: Duration::from_millis(333), - justification_generation_period: GRANDPA_JUSTIFICATION_PERIOD, - name: Some(name), - observer_enabled: false, - keystore, - local_role: role, - telemetry: telemetry.as_ref().map(|x| x.handle()), - protocol_name: grandpa_protocol_name, - }; - - // start the full GRANDPA voter - // NOTE: non-authorities could run the GRANDPA observer protocol, but at - // this point the full voter should provide better guarantees of block - // and vote data availability than the observer. The observer has not - // been tested extensively yet and having most nodes in a network run it - // could lead to finality stalls. - let grandpa_config = sc_consensus_grandpa::GrandpaParams { - config: grandpa_config, - link: grandpa_link, - network, - sync: Arc::new(sync_service), - notification_service: grandpa_notification_service, - voting_rule: sc_consensus_grandpa::VotingRulesBuilder::default().build(), - prometheus_registry, - shared_voter_state: SharedVoterState::empty(), - telemetry: telemetry.as_ref().map(|x| x.handle()), - offchain_tx_pool_factory: OffchainTransactionPoolFactory::new(transaction_pool), - }; - - // the GRANDPA voter task is considered infallible, i.e. - // if it fails we take down the service with it. - task_manager.spawn_essential_handle().spawn_blocking( - "grandpa-voter", - None, - sc_consensus_grandpa::run_grandpa_voter(grandpa_config)?, - ); - } - - Ok(task_manager) + Ok((slot, timestamp)) + }, + force_authoring, + backoff_authoring_blocks, + keystore: keystore_container.keystore(), + sync_oracle: sync_service.clone(), + justification_sync_link: sync_service.clone(), + block_proposal_slot_portion: SlotProportion::new(2f32 / 3f32), + max_block_proposal_slot_portion: None, + telemetry: telemetry.as_ref().map(|x| x.handle()), + compatibility_mode: Default::default(), + }, + )?; + + // the AURA authoring task is considered essential, i.e. if it + // fails we take down the service with it. + task_manager + .spawn_essential_handle() + .spawn_blocking("aura", Some("block-authoring"), aura); + } + + if enable_grandpa { + // if the node isn't actively participating in consensus then it doesn't + // need a keystore, regardless of which protocol we use below. + let keystore = if role.is_authority() { + Some(keystore_container.keystore()) + } else { + None + }; + + let grandpa_config = sc_consensus_grandpa::Config { + // FIXME #1578 make this available through chainspec + gossip_duration: Duration::from_millis(333), + justification_generation_period: GRANDPA_JUSTIFICATION_PERIOD, + name: Some(name), + observer_enabled: false, + keystore, + local_role: role, + telemetry: telemetry.as_ref().map(|x| x.handle()), + protocol_name: grandpa_protocol_name, + }; + + // start the full GRANDPA voter + // NOTE: non-authorities could run the GRANDPA observer protocol, but at + // this point the full voter should provide better guarantees of block + // and vote data availability than the observer. The observer has not + // been tested extensively yet and having most nodes in a network run it + // could lead to finality stalls. + let grandpa_config = sc_consensus_grandpa::GrandpaParams { + config: grandpa_config, + link: grandpa_link, + network, + sync: Arc::new(sync_service), + notification_service: grandpa_notification_service, + voting_rule: sc_consensus_grandpa::VotingRulesBuilder::default().build(), + prometheus_registry, + shared_voter_state: SharedVoterState::empty(), + telemetry: telemetry.as_ref().map(|x| x.handle()), + offchain_tx_pool_factory: OffchainTransactionPoolFactory::new(transaction_pool), + }; + + // the GRANDPA voter task is considered infallible, i.e. + // if it fails we take down the service with it. + task_manager.spawn_essential_handle().spawn_blocking( + "grandpa-voter", + None, + sc_consensus_grandpa::run_grandpa_voter(grandpa_config)?, + ); + } + + Ok(task_manager) } diff --git a/pallets/assets/src/benchmarking.rs b/pallets/assets/src/benchmarking.rs index cc6f440..cefdaa7 100644 --- a/pallets/assets/src/benchmarking.rs +++ b/pallets/assets/src/benchmarking.rs @@ -6,42 +6,36 @@ use frame_benchmarking::v2::*; use frame_system::RawOrigin; #[benchmarks] -mod benchmark{ +mod benchmark { use super::*; #[benchmark] - fn deposit(){ - let caller : T::AccountId = whitelisted_caller(); + fn deposit() { + let caller: T::AccountId = whitelisted_caller(); let asset_id: u32 = 0u32; let amount: u128 = 1000u128; #[extrinsic_call] deposit(RawOrigin::Signed(caller.clone()), asset_id, amount); - assert_eq!( - FreeBalance::::get(&caller, asset_id), - amount - ); + assert_eq!(FreeBalance::::get(&caller, asset_id), amount); } #[benchmark] - fn withdraw(){ - let caller: T::AccountId = whitelisted_caller(); - let asset_id = 0u32; - let amount = 1000u128; - - //fake deposit - FreeBalance::::insert(&caller, asset_id, amount); - - //now withdraw - #[extrinsic_call] - withdraw(RawOrigin::Signed(caller.clone()), asset_id, amount); - - assert_eq!( - FreeBalance::::get(&caller, asset_id), - 0 - ); + fn withdraw() { + let caller: T::AccountId = whitelisted_caller(); + let asset_id = 0u32; + let amount = 1000u128; + + //fake deposit + FreeBalance::::insert(&caller, asset_id, amount); + + //now withdraw + #[extrinsic_call] + withdraw(RawOrigin::Signed(caller.clone()), asset_id, amount); + + assert_eq!(FreeBalance::::get(&caller, asset_id), 0); } impl_benchmark_test_suite!(Assets, crate::mock::new_test_ext(), crate::mock::Test); -} \ No newline at end of file +} diff --git a/pallets/assets/src/lib.rs b/pallets/assets/src/lib.rs index e8fdb41..3d890ba 100644 --- a/pallets/assets/src/lib.rs +++ b/pallets/assets/src/lib.rs @@ -1,9 +1,8 @@ //ensures it compiles to wasm -#![cfg_attr(not(feature="std"), no_std)] +#![cfg_attr(not(feature = "std"), no_std)] pub use pallet::*; - #[cfg(test)] mod mock; @@ -22,29 +21,35 @@ pub const ETH: u32 = 1; pub mod pallet { use super::*; use frame_support::{dispatch::DispatchResult, pallet_macros, pallet_prelude::*}; - use frame_system::{pallet_prelude::{OriginFor, *}}; + use frame_system::pallet_prelude::{OriginFor, *}; #[pallet::pallet] pub struct Pallet(_); - // UserID -> Token -> Value + // UserID -> Token -> Value #[pallet::storage] pub type FreeBalance = StorageDoubleMap< - _, - Blake2_128Concat, T::AccountId, - Blake2_128Concat, u32, - u128, - ValueQuery>; - - //same with locked balance + _, + Blake2_128Concat, + T::AccountId, + Blake2_128Concat, + u32, + u128, + ValueQuery, + >; + + //same with locked balance // userid --> token --> value #[pallet::storage] - pub type LockedBalance = StorageDoubleMap< - _, - Blake2_128Concat, T::AccountId, - Blake2_128Concat, u32, - u128, - ValueQuery>; + pub type LockedBalance = StorageDoubleMap< + _, + Blake2_128Concat, + T::AccountId, + Blake2_128Concat, + u32, + u128, + ValueQuery, + >; #[pallet::config] pub trait Config: frame_system::Config { @@ -55,18 +60,33 @@ pub mod pallet { #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { - Deposited { user: T::AccountId, asset_id: u32, amount: u128 }, - Withdrawn { user: T::AccountId, asset_id: u32, amount: u128 }, - Locked { user: T::AccountId, asset_id: u32, amount: u128 }, - Unlocked { user: T::AccountId, asset_id: u32, amount: u128 }, - Transferred { - from: T::AccountId, - to: T::AccountId, - asset_id: u32, - amount: u128 + Deposited { + user: T::AccountId, + asset_id: u32, + amount: u128, + }, + Withdrawn { + user: T::AccountId, + asset_id: u32, + amount: u128, + }, + Locked { + user: T::AccountId, + asset_id: u32, + amount: u128, + }, + Unlocked { + user: T::AccountId, + asset_id: u32, + amount: u128, + }, + Transferred { + from: T::AccountId, + to: T::AccountId, + asset_id: u32, + amount: u128, }, } - #[pallet::error] pub enum Error { @@ -78,105 +98,103 @@ pub mod pallet { //Now we write the extrinsincs deposit, withdraw, lock and unlock & also transfer #[pallet::call] - impl Pallet { + impl Pallet { #[pallet::call_index(0)] #[pallet::weight(T::WeightInfo::deposit())] - pub fn deposit( - origin: OriginFor, - asset_id: u32, - amount: u128, - ) -> DispatchResult { - let who = ensure_signed(origin)?; + pub fn deposit(origin: OriginFor, asset_id: u32, amount: u128) -> DispatchResult { + let who = ensure_signed(origin)?; - ensure!(amount > 0, Error::::AmountZero); - ensure!(asset_id == 0 || asset_id == 1, Error::::InvalidAsset); + ensure!(amount > 0, Error::::AmountZero); + ensure!(asset_id == 0 || asset_id == 1, Error::::InvalidAsset); - FreeBalance::::mutate(who.clone(), asset_id, |balance| { - *balance = balance.saturating_add(amount); - }); + FreeBalance::::mutate(who.clone(), asset_id, |balance| { + *balance = balance.saturating_add(amount); + }); - Self::deposit_event(Event::Deposited {user: who,asset_id: asset_id,amount: amount }); - Ok(()) + Self::deposit_event(Event::Deposited { + user: who, + asset_id: asset_id, + amount: amount, + }); + Ok(()) } #[pallet::call_index(1)] #[pallet::weight(T::WeightInfo::withdraw())] - pub fn withdraw( - origin: OriginFor, - asset_id: u32, - amount: u128, - ) -> DispatchResult { + pub fn withdraw(origin: OriginFor, asset_id: u32, amount: u128) -> DispatchResult { let who = ensure_signed(origin)?; ensure!(amount > 0, Error::::AmountZero); - ensure!(asset_id == USDT || asset_id == ETH, Error::::InvalidAsset); + ensure!( + asset_id == USDT || asset_id == ETH, + Error::::InvalidAsset + ); FreeBalance::::try_mutate(who.clone(), asset_id, |balance| { ensure!(*balance >= amount, Error::::InsufficientFreeBalance); *balance = balance.saturating_sub(amount); - Ok::<_,DispatchError>(()) + Ok::<_, DispatchError>(()) })?; - Self::deposit_event( Event::Withdrawn {user:who,asset_id: asset_id,amount: amount }); + Self::deposit_event(Event::Withdrawn { + user: who, + asset_id: asset_id, + amount: amount, + }); Ok(()) } } impl Pallet { - /// Get free balance (helper for tests) pub fn get_free_balance(user: &T::AccountId, asset_id: u32) -> u128 { FreeBalance::::get(user, asset_id) } - + /// Get locked balance (helper for tests) pub fn get_locked_balance(user: &T::AccountId, asset_id: u32) -> u128 { LockedBalance::::get(user, asset_id) } // locking funds for when trading happens, user cannot simply just withdraw stuff - pub fn lock_funds( - user: &T::AccountId, - asset_id: u32, - amount: u128, - ) -> DispatchResult { + pub fn lock_funds(user: &T::AccountId, asset_id: u32, amount: u128) -> DispatchResult { // we move from freebalance and shift to lockedbalance FreeBalance::::try_mutate(user, asset_id, |balance| { ensure!(*balance >= amount, Error::::InsufficientFreeBalance); *balance = balance.saturating_sub(amount); - Ok::<_,DispatchError>(()) + Ok::<_, DispatchError>(()) })?; - LockedBalance::::mutate(user,asset_id, |balance| { + LockedBalance::::mutate(user, asset_id, |balance| { *balance = balance.saturating_add(amount); }); - Self::deposit_event( Event::Locked {user: (*user).clone(), asset_id: asset_id, amount: amount}); + Self::deposit_event(Event::Locked { + user: (*user).clone(), + asset_id: asset_id, + amount: amount, + }); Ok(()) } // This can happen when we say cancel and order - pub fn unlock_funds( - user: &T::AccountId, - asset_id: u32, - amount: u128, - ) -> DispatchResult { + pub fn unlock_funds(user: &T::AccountId, asset_id: u32, amount: u128) -> DispatchResult { // Move from locked to free LockedBalance::::try_mutate(user, asset_id, |locked| { ensure!(*locked >= amount, Error::::InsufficientLockedBalance); *locked = locked.saturating_sub(amount); Ok::<_, DispatchError>(()) })?; - + FreeBalance::::mutate(user, asset_id, |balance| { *balance = balance.saturating_add(amount); }); - + Self::deposit_event(Event::Unlocked { user: user.clone(), asset_id, amount, }); - + Ok(()) } @@ -186,23 +204,26 @@ pub mod pallet { asset_id: u32, amount: u128, ) -> DispatchResult { - - // remove from transferee + // remove from transferee LockedBalance::::try_mutate(from, asset_id, |balance| { ensure!(*balance >= amount, Error::::InsufficientLockedBalance); *balance = balance.saturating_sub(amount); - Ok::<_,DispatchError>(()) + Ok::<_, DispatchError>(()) })?; //move it to transferred .ie to account FreeBalance::::mutate(to, asset_id, |balance| { *balance = balance.saturating_add(amount) - }); - Self::deposit_event( Event::Transferred { from: from.clone(), to: to.clone(), asset_id: asset_id, amount: amount}); + Self::deposit_event(Event::Transferred { + from: from.clone(), + to: to.clone(), + asset_id: asset_id, + amount: amount, + }); Ok(()) } } -} \ No newline at end of file +} diff --git a/pallets/assets/src/mock.rs b/pallets/assets/src/mock.rs index 121ef3f..af40d1c 100644 --- a/pallets/assets/src/mock.rs +++ b/pallets/assets/src/mock.rs @@ -1,6 +1,6 @@ +use crate as pallet_assets; use frame_support::derive_impl; use sp_runtime::BuildStorage; -use crate as pallet_assets; type Block = frame_system::mocking::MockBlock; @@ -16,7 +16,7 @@ mod runtime { RuntimeHoldReason, RuntimeSlashReason, RuntimeLockId, - RuntimeTask, + RuntimeTask )] pub struct Test; @@ -28,11 +28,11 @@ mod runtime { } #[derive_impl(frame_system::config_preludes::TestDefaultConfig)] -impl frame_system::Config for Test{ +impl frame_system::Config for Test { type Block = Block; } -impl pallet_assets::Config for Test{ +impl pallet_assets::Config for Test { type RuntimeEvent = RuntimeEvent; type WeightInfo = pallet_assets::weights::SubstrateWeight; } diff --git a/pallets/assets/src/tests.rs b/pallets/assets/src/tests.rs index 8fb0d16..4f8550b 100644 --- a/pallets/assets/src/tests.rs +++ b/pallets/assets/src/tests.rs @@ -1,6 +1,6 @@ // pallets/assets/src/tests.rs -use crate::{mock::*, Error, Event, USDT, ETH}; +use crate::{mock::*, Error, Event, ETH, USDT}; use frame_support::{assert_noop, assert_ok}; #[test] @@ -8,20 +8,21 @@ fn deposit_works() { new_test_ext().execute_with(|| { // Go to block 1 (for events) System::set_block_number(1); - + // Deposit 1000 USDT assert_ok!(Assets::deposit(RuntimeOrigin::signed(1), USDT, 1000)); - + // Check balance assert_eq!(Assets::get_free_balance(&1, USDT), 1000); - + // Check event System::assert_has_event( - Event::Deposited { - user: 1, - asset_id: USDT, - amount: 1000 - }.into() + Event::Deposited { + user: 1, + asset_id: USDT, + amount: 1000, + } + .into(), ); }); } @@ -50,13 +51,13 @@ fn deposit_invalid_asset_fails() { fn withdraw_works() { new_test_ext().execute_with(|| { System::set_block_number(1); - + // Setup: deposit first assert_ok!(Assets::deposit(RuntimeOrigin::signed(1), USDT, 1000)); - + // Withdraw 300 assert_ok!(Assets::withdraw(RuntimeOrigin::signed(1), USDT, 300)); - + // Check balance assert_eq!(Assets::get_free_balance(&1, USDT), 700); }); @@ -77,15 +78,15 @@ fn withdraw_insufficient_balance_fails() { fn lock_and_unlock_works() { new_test_ext().execute_with(|| { System::set_block_number(1); - + // Deposit 1000 assert_ok!(Assets::deposit(RuntimeOrigin::signed(1), ETH, 1000)); - + // Lock 400 assert_ok!(Assets::lock_funds(&1, ETH, 400)); assert_eq!(Assets::get_free_balance(&1, ETH), 600); assert_eq!(Assets::get_locked_balance(&1, ETH), 400); - + // Unlock 200 assert_ok!(Assets::unlock_funds(&1, ETH, 200)); assert_eq!(Assets::get_free_balance(&1, ETH), 800); @@ -97,16 +98,16 @@ fn lock_and_unlock_works() { fn transfer_locked_works() { new_test_ext().execute_with(|| { System::set_block_number(1); - + // User 1 deposits and locks assert_ok!(Assets::deposit(RuntimeOrigin::signed(1), USDT, 1000)); assert_ok!(Assets::lock_funds(&1, USDT, 500)); - + // Transfer locked from user 1 to user 2 assert_ok!(Assets::transfer_locked(&1, &2, USDT, 300)); - + // Check balances - assert_eq!(Assets::get_locked_balance(&1, USDT), 200); // 500 - 300 - assert_eq!(Assets::get_locked_balance(&2, USDT), 300); // Received as free + assert_eq!(Assets::get_locked_balance(&1, USDT), 200); // 500 - 300 + assert_eq!(Assets::get_locked_balance(&2, USDT), 300); // Received as free }); -} \ No newline at end of file +} diff --git a/pallets/orderbook/src/benchmarking.rs b/pallets/orderbook/src/benchmarking.rs index 354fde5..d556087 100644 --- a/pallets/orderbook/src/benchmarking.rs +++ b/pallets/orderbook/src/benchmarking.rs @@ -1 +1,262 @@ -// to be impl \ No newline at end of file +#![cfg(feature = "runtime-benchmarks")] +use frame_benchmarking::v2::*; + +#[benchmarks] +mod benchmarks { + use super::*; + use crate::types::{OrderSide, OrderType}; + use crate::Pallet as Orderbook; + use crate::{Call, Config, Pallet}; + use frame_support::assert_ok; + use frame_support::traits::Hooks; + use frame_system::RawOrigin; + use pallet_assets::{ETH, USDT}; + + // Type alias for cleaner code + type AccountIdOf = ::AccountId; + + /// Helper to create a funded account with USDT and ETH + fn funded_account(name: &'static str, index: u32) -> AccountIdOf { + let caller: AccountIdOf = account(name, index, 0); + + // Deposit USDT (for buying) + assert_ok!(pallet_assets::Pallet::::deposit( + RawOrigin::Signed(caller.clone()).into(), + USDT, + 1_000_000_000u128, + )); + + // Deposit ETH (for selling) + assert_ok!(pallet_assets::Pallet::::deposit( + RawOrigin::Signed(caller.clone()).into(), + ETH, + 1_000_000u128, + )); + + caller + } + + /// Helper to setup matching orders + fn setup_matching_orders(num_bids: u32, num_asks: u32, price: u128) { + for i in 0..num_bids { + let buyer = funded_account::("buyer", i); + assert_ok!(Orderbook::::place_order( + RawOrigin::Signed(buyer).into(), + OrderSide::Buy, + price, + 10u128, + OrderType::Limit + )); + } + + for i in 0..num_asks { + let seller = funded_account::("seller", i); + assert_ok!(Orderbook::::place_order( + RawOrigin::Signed(seller).into(), + OrderSide::Sell, + price, + 10u128, + OrderType::Limit + )); + } + } + + /// Helper to setup non-matching orders + fn setup_non_matching_orders(num_bids: u32, num_asks: u32) { + for i in 0..num_bids { + let buyer = funded_account::("buyer", i); + assert_ok!(Orderbook::::place_order( + RawOrigin::Signed(buyer).into(), + OrderSide::Buy, + 90u128, + 10u128, + OrderType::Limit + )); + } + + for i in 0..num_asks { + let seller = funded_account::("seller", i); + assert_ok!(Orderbook::::place_order( + RawOrigin::Signed(seller).into(), + OrderSide::Sell, + 110u128, + 10u128, + OrderType::Limit + )); + } + } + + /// Helper to setup cancellations + fn setup_cancellations(num_cancellations: u32) { + for i in 0..num_cancellations { + let user = funded_account::("user_cancel", i); + + // Get the CURRENT order_id before placing + let order_id_before = Pallet::::next_order_id(); + + // Place order + assert_ok!(Pallet::::place_order( + RawOrigin::Signed(user.clone()).into(), + OrderSide::Buy, + 100u128, + 10u128, + OrderType::Limit + )); + + // The order_id that was just created is order_id_before + // (because NextOrderId was incremented AFTER the order was placed) + assert_ok!(Pallet::::cancel_order( + RawOrigin::Signed(user).into(), + order_id_before, // Use the captured order_id + )); + } + } + + // ======================================== + // EXTRINSIC BENCHMARKS + // ======================================== + + #[benchmark] + fn place_order() { + let caller = funded_account::("caller", 0); + + #[extrinsic_call] + place_order( + RawOrigin::Signed(caller.clone()), + OrderSide::Buy, + 100u128, + 10u128, + OrderType::Limit, + ); + + assert_eq!(Orderbook::::next_order_id(), 1); + } + + #[benchmark] + fn cancel_order() { + let caller = funded_account::("caller", 0); + + assert_ok!(Orderbook::::place_order( + RawOrigin::Signed(caller.clone()).into(), + OrderSide::Buy, + 100u128, + 10u128, + OrderType::Limit + )); + + let order_id = 0; + + #[extrinsic_call] + cancel_order(RawOrigin::Signed(caller.clone()), order_id); + + assert_eq!(Orderbook::::get_pending_cancellations().len(), 1); + } + + // ======================================== + // ON_FINALIZE BENCHMARKS + // ======================================== + + #[benchmark] + fn on_finalize_empty() { + #[block] + { + Orderbook::::on_finalize(1u32.into()); + } + } + + #[benchmark] + fn on_finalize_with_matches(b: Linear<1, 50>, a: Linear<1, 50>) { + setup_matching_orders::(b, a, 100u128); + + #[block] + { + Orderbook::::on_finalize(1u32.into()); + } + + assert!(Orderbook::::next_trade_id() > 0); + } + + #[benchmark] + fn on_finalize_no_matches(b: Linear<1, 50>, a: Linear<1, 50>) { + setup_non_matching_orders::(b, a); + + #[block] + { + Orderbook::::on_finalize(1u32.into()); + } + + assert!(Orderbook::::get_bids_at_price(90).len() > 0); + assert!(Orderbook::::get_asks_at_price(110).len() > 0); + } + + #[benchmark] + fn on_finalize_with_cancellations(c: Linear<1, 50>) { + setup_cancellations::(c); + + #[block] + { + Orderbook::::on_finalize(1u32.into()); + } + + assert_eq!(Orderbook::::get_pending_cancellations().len(), 0); + } + + #[benchmark] + fn on_finalize_persistent_matching(p: Linear<1, 20>, n: Linear<1, 20>) { + for i in 0..p { + let seller = funded_account::("persistent_seller", i); + assert_ok!(Orderbook::::place_order( + RawOrigin::Signed(seller).into(), + OrderSide::Sell, + 100u128, + 10u128, + OrderType::Limit + )); + } + + Orderbook::::on_finalize(1u32.into()); + + for i in 0..n { + let buyer = funded_account::("new_buyer", i); + assert_ok!(Orderbook::::place_order( + RawOrigin::Signed(buyer).into(), + OrderSide::Buy, + 100u128, + 10u128, + OrderType::Limit + )); + } + + #[block] + { + Orderbook::::on_finalize(2u32.into()); + } + + assert!(Orderbook::::next_trade_id() > 0); + } + + #[benchmark] + fn on_finalize_complex(m: Linear<1, 20>, n: Linear<1, 20>, c: Linear<1, 10>) { + setup_matching_orders::(m, m, 100u128); + + for i in 0..n { + let buyer = funded_account::("buyer_high", i); + assert_ok!(Orderbook::::place_order( + RawOrigin::Signed(buyer).into(), + OrderSide::Buy, + 90u128, + 10u128, + OrderType::Limit + )); + } + + setup_cancellations::(c); + + #[block] + { + Orderbook::::on_finalize(1u32.into()); + } + } + + impl_benchmark_test_suite!(Orderbook, crate::mock::new_test_ext(), crate::mock::Test); +} diff --git a/pallets/orderbook/src/engine.rs b/pallets/orderbook/src/engine.rs index dbe9c77..b7c1e13 100644 --- a/pallets/orderbook/src/engine.rs +++ b/pallets/orderbook/src/engine.rs @@ -1,8 +1,5 @@ use codec::{Decode, Encode}; -use frame_support::{ - ensure, - pallet_prelude::*, -}; +use frame_support::{ensure, pallet_prelude::*}; use sp_runtime::traits::Zero; use sp_std::{collections::btree_map::BTreeMap, vec::Vec}; @@ -10,22 +7,19 @@ use sp_std::{collections::btree_map::BTreeMap, vec::Vec}; use crate::types::*; use frame_system::Config; - - // This will match with the cache structure pub fn match_pending_internal( pending_bids: BTreeMap>, pending_asks: BTreeMap>, orders_map: &mut BTreeMap>, ) -> Result<(Vec>, Vec), DispatchError> { - let mut bid_book = pending_bids; let mut ask_book = pending_asks; - let mut trades = Vec::new(); + let mut trades = Vec::new(); let mut all_pending_ids = Vec::new(); - for (_price, order_ids) in bid_book.iter(){ + for (_price, order_ids) in bid_book.iter() { all_pending_ids.extend(order_ids.clone()); } @@ -43,17 +37,15 @@ pub fn match_pending_internal( remove_from_orderbook(order_id, &order, &mut bid_book, &mut ask_book); - let order_trades = match order.side{ + let order_trades = match order.side { OrderSide::Buy => match_buy_order(&mut order, &mut ask_book, orders_map)?, OrderSide::Sell => match_sell_order(&mut order, &mut bid_book, orders_map)?, }; - trades.extend(order_trades); - - + trades.extend(order_trades); orders_map.insert(order_id, order.clone()); - + if order.status != OrderStatus::Filled { add_order_to_book(&order, &mut bid_book, &mut ask_book); } @@ -66,20 +58,19 @@ pub fn match_pending_internal( for (_price, ids) in ask_book.iter() { unmatched.extend(ids.clone()); } - + Ok((trades, unmatched)) } -pub fn match_persistent_storage( +pub fn match_persistent_storage( persistent_bids: &mut BTreeMap>, persistent_asks: &mut BTreeMap>, unmatched: Vec, orders_map: &mut BTreeMap>, -) -> Result>, DispatchError>{ - +) -> Result>, DispatchError> { let mut trades = Vec::new(); - for order_id in unmatched.iter(){ + for order_id in unmatched.iter() { let mut order = match orders_map.get(&order_id) { Some(o) => o.clone(), None => continue, @@ -87,7 +78,7 @@ pub fn match_persistent_storage( let order_trades = match order.side { OrderSide::Buy => match_buy_order(&mut order, persistent_asks, orders_map), - OrderSide::Sell => match_sell_order(&mut order, persistent_bids, orders_map) + OrderSide::Sell => match_sell_order(&mut order, persistent_bids, orders_map), }; trades.extend(order_trades.unwrap()); @@ -106,31 +97,33 @@ fn remove_from_orderbook( order: &Order, bid_book: &mut BTreeMap>, ask_book: &mut BTreeMap>, -) { - let book = match order.side { - OrderSide::Buy => bid_book, - OrderSide::Sell => ask_book, - }; - - if let Some(ids) = book.get_mut(&order.price) { - ids.retain(|id| *id != order_id); - if ids.is_empty() { - book.remove(&order.price); - } +) { + let book = match order.side { + OrderSide::Buy => bid_book, + OrderSide::Sell => ask_book, + }; + + if let Some(ids) = book.get_mut(&order.price) { + ids.retain(|id| *id != order_id); + if ids.is_empty() { + book.remove(&order.price); + } } } -fn add_order_to_book( +fn add_order_to_book( order: &Order, bid_book: &mut BTreeMap>, - ask_book: &mut BTreeMap> -){ + ask_book: &mut BTreeMap>, +) { let book = match order.side { OrderSide::Buy => bid_book, OrderSide::Sell => ask_book, }; - book.entry(order.price).or_insert_with(Vec::new).push(order.order_id); + book.entry(order.price) + .or_insert_with(Vec::new) + .push(order.order_id); } fn match_buy_order( @@ -138,173 +131,161 @@ fn match_buy_order( ask_book: &mut BTreeMap>, orders_map: &mut BTreeMap>, ) -> Result>, DispatchError> { - let mut trades = Vec::new(); let mut prices_to_remove = Vec::new(); - + // Get all ask prices sorted (lowest first) let ask_prices: Vec = ask_book.keys().cloned().collect(); - + for price in ask_prices.iter() { - // Check if we can match at this price match buy_order.order_type { OrderType::Market => { // Market orders match at any price - }, + } OrderType::Limit => { if buy_order.price < *price { break; // Too expensive, stop } - }, + } } - + // Check if buy order still needs filling if remaining_quantity(buy_order) == 0 { break; } - + // Get sell orders at this price level if let Some(sell_order_ids) = ask_book.get_mut(price) { - let mut indices_to_remove = Vec::new(); - + // Match with each sell order (FIFO - price-time priority) for (idx, sell_order_id) in sell_order_ids.iter().enumerate() { - // Get the sell order let mut sell_order = match orders_map.get(sell_order_id) { Some(o) => o.clone(), None => continue, }; - + // Execute trade at this price level (maker's price) let trade = execute_trade(buy_order, &mut sell_order, *price)?; trades.push(trade); - + // Update sell order in orders_map orders_map.insert(*sell_order_id, sell_order.clone()); - + // If sell order is filled, mark for removal if sell_order.status == OrderStatus::Filled { indices_to_remove.push(idx); } - + // If buy order is filled, stop matching if buy_order.status == OrderStatus::Filled { break; } } - + // Remove filled orders (reverse to maintain indices) for idx in indices_to_remove.iter().rev() { sell_order_ids.remove(*idx); } - + // If no orders left at this price, mark for removal if sell_order_ids.is_empty() { prices_to_remove.push(*price); } } } - + // Clean up empty price levels for price in prices_to_remove { ask_book.remove(&price); } - + Ok(trades) } - fn match_sell_order( sell_order: &mut Order, bid_book: &mut BTreeMap>, orders_map: &mut BTreeMap>, ) -> Result>, DispatchError> { - let mut trades = Vec::new(); let mut prices_to_remove = Vec::new(); - + // Get all bid prices sorted (highest first) let mut bid_prices: Vec = bid_book.keys().cloned().collect(); bid_prices.sort_by(|a, b| b.cmp(a)); // Reverse sort - + for price in bid_prices.iter() { - // Check if we can match at this price match sell_order.order_type { OrderType::Market => { // Market orders match at any price - }, + } OrderType::Limit => { if sell_order.price > *price { break; // Too cheap, stop } - }, + } } - + // Check if sell order still needs filling if remaining_quantity(sell_order) == 0 { break; } - + // Get buy orders at this price level if let Some(buy_order_ids) = bid_book.get_mut(price) { - let mut indices_to_remove = Vec::new(); - + // Match with each buy order (FIFO - price-time priority) for (idx, buy_order_id) in buy_order_ids.iter().enumerate() { - // Get the buy order let mut buy_order = match orders_map.get(buy_order_id) { Some(o) => o.clone(), None => continue, }; - + // Execute trade at this price level (maker's price) let trade = execute_trade(&mut buy_order, sell_order, *price)?; trades.push(trade); - + // Update buy order in orders_map orders_map.insert(*buy_order_id, buy_order.clone()); - + // If buy order is filled, mark for removal if buy_order.status == OrderStatus::Filled { indices_to_remove.push(idx); } - + // If sell order is filled, stop matching if sell_order.status == OrderStatus::Filled { break; } } - + // Remove filled orders (reverse to maintain indices) for idx in indices_to_remove.iter().rev() { buy_order_ids.remove(*idx); } - + // If no orders left at this price, mark for removal if buy_order_ids.is_empty() { prices_to_remove.push(*price); } } } - + // Clean up empty price levels for price in prices_to_remove { bid_book.remove(&price); } - + Ok(trades) } - -fn remaining_quantity( - order: &mut Order -) -> Amount { +fn remaining_quantity(order: &mut Order) -> Amount { order.quantity.saturating_sub(order.filled_quantity) } @@ -313,13 +294,15 @@ fn execute_trade( sell_order: &mut Order, match_price: Amount, ) -> Result, DispatchError> { - let buy_remaining = remaining_quantity(buy_order); let sell_remaining = remaining_quantity(sell_order); let trade_qty = buy_remaining.min(sell_remaining); // update buy order - buy_order.filled_quantity = buy_order.filled_quantity.checked_add(trade_qty).ok_or("ArithmeticOverFlow")?; + buy_order.filled_quantity = buy_order + .filled_quantity + .checked_add(trade_qty) + .ok_or("ArithmeticOverFlow")?; if buy_order.filled_quantity == buy_order.quantity { buy_order.status = OrderStatus::Filled; @@ -328,7 +311,10 @@ fn execute_trade( } //update sell order - sell_order.filled_quantity = sell_order.filled_quantity.checked_add(trade_qty).ok_or("ArithmeticOverFlow")?; + sell_order.filled_quantity = sell_order + .filled_quantity + .checked_add(trade_qty) + .ok_or("ArithmeticOverFlow")?; if sell_order.filled_quantity == sell_order.quantity { sell_order.status = OrderStatus::Filled; @@ -338,7 +324,7 @@ fn execute_trade( //Everything updated, now to emit the trades Ok(Trade { - trade_id:0, // placeholder + trade_id: 0, // placeholder buyer: buy_order.trader.clone(), seller: sell_order.trader.clone(), buy_order_id: buy_order.order_id, @@ -349,23 +335,20 @@ fn execute_trade( } // now for cancellation -pub fn process_cancellations( +pub fn process_cancellations( order_ids: Vec, bid_book: &mut BTreeMap>, ask_book: &mut BTreeMap>, - orders_map: &mut BTreeMap> + orders_map: &mut BTreeMap>, ) -> Result<(), DispatchError> { - for order_id in order_ids { if let Some(mut order) = orders_map.get(&order_id).cloned() { - order.status = OrderStatus::Cancelled; remove_from_orderbook(order_id, &order, bid_book, ask_book); orders_map.insert(order_id, order.clone()); - } } Ok(()) -} \ No newline at end of file +} diff --git a/pallets/orderbook/src/lib.rs b/pallets/orderbook/src/lib.rs index db87afb..f51ce4c 100644 --- a/pallets/orderbook/src/lib.rs +++ b/pallets/orderbook/src/lib.rs @@ -1,8 +1,7 @@ - #![cfg_attr(not(feature = "std"), no_std)] #![allow(ambiguous_glob_reexports)] -pub mod types; mod engine; +pub mod types; pub use pallet::*; //pub use crate::types; //pub use pallet_assets::*; @@ -13,9 +12,10 @@ mod mock; #[cfg(test)] mod tests; -#[cfg(feature="runtime-benchmarks")] +#[cfg(feature = "runtime-benchmarks")] mod benchmarking; - +pub mod weights; +pub use weights::WeightInfo; #[frame_support::pallet] pub mod pallet { @@ -24,14 +24,18 @@ pub mod pallet { use core::u32; //use super::*; - use crate::{engine::*, types::{Amount, Order, OrderId, OrderSide, OrderStatus, OrderType, Trade, TradeId}}; - - use frame_support::{Blake2_128Concat, pallet_prelude:: *}; - use frame_system::pallet_prelude::{OriginFor, *}; - use sp_core::Get; + use crate::{ + engine::*, + types::{Amount, Order, OrderId, OrderSide, OrderStatus, OrderType, Trade, TradeId}, + weights::WeightInfo, + }; + + use assets::{ETH, USDT}; + use frame_support::{pallet_prelude::*, Blake2_128Concat}; + use frame_system::pallet_prelude::{OriginFor, *}; use pallet_assets as assets; + use sp_core::Get; use sp_std::{collections::btree_map::BTreeMap, vec::Vec}; - use assets::{USDT,ETH}; //use assets::*; //use sp_runtime::legacy::byte_sized_error::DispatchError; @@ -40,7 +44,7 @@ pub mod pallet { #[pallet::config] pub trait Config: frame_system::Config + pallet_assets::Config { - type RuntimeEvent: From> + IsType<::RuntimeEvent>; + type RuntimeEvent: From> + IsType<::RuntimeEvent>; // maximum orders at any price level not sure if needed. this will be on the blockOrders cache #[pallet::constant] @@ -56,40 +60,31 @@ pub mod pallet { #[pallet::constant] type MaxUserOrders: Get; + type WeightInfo: WeightInfo; } - // =========================== - // Persisten storage - // =========================== + // =========================== + // Persisten storage + // =========================== - // not sure if this needed yet, so just keeping it - #[pallet::storage] + // not sure if this needed yet, so just keeping it + #[pallet::storage] pub type Orders = StorageMap<_, Blake2_128Concat, OrderId, Order, OptionQuery>; #[pallet::storage] pub type Trades = StorageMap<_, Blake2_128Concat, TradeId, Trade, OptionQuery>; #[pallet::storage] - pub type Bids = StorageMap< - _, - Blake2_128Concat, - Amount, - BoundedVec, - ValueQuery, - >; - + pub type Bids = + StorageMap<_, Blake2_128Concat, Amount, BoundedVec, ValueQuery>; + #[pallet::storage] - pub type Asks = StorageMap< - _, - Blake2_128Concat, - Amount, - BoundedVec, - ValueQuery, - >; + pub type Asks = + StorageMap<_, Blake2_128Concat, Amount, BoundedVec, ValueQuery>; // =========================== - // Cache - // =========================== + // Cache + // =========================== #[pallet::storage] pub type PendingAsks = StorageMap< @@ -102,25 +97,25 @@ pub mod pallet { #[pallet::storage] pub type PendingBids = StorageMap< - _, - Blake2_128Concat, - Amount, - BoundedVec, - ValueQuery, - >; - - #[pallet::storage] - pub type PendingCancellations = StorageValue<_, BoundedVec, ValueQuery>; - - + _, + Blake2_128Concat, + Amount, + BoundedVec, + ValueQuery, + >; + + #[pallet::storage] + pub type PendingCancellations = + StorageValue<_, BoundedVec, ValueQuery>; + //Keeping this so that users can easily access their orders #[pallet::storage] pub type UserOrders = StorageMap< _, Blake2_128Concat, T::AccountId, - BoundedVec, - ValueQuery + BoundedVec, + ValueQuery, >; #[pallet::storage] @@ -131,14 +126,14 @@ pub mod pallet { #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] - pub enum Event { - OrderPlaced{ + pub enum Event { + OrderPlaced { order_id: OrderId, side: OrderSide, price: Amount, - quantity: Amount + quantity: Amount, }, - TradeExecuted{ + TradeExecuted { trade_id: TradeId, buy_order_id: OrderId, sell_order_id: OrderId, @@ -146,7 +141,6 @@ pub mod pallet { seller: T::AccountId, price: Amount, quantity: Amount, - }, OrderCancelled { order_id: OrderId, @@ -170,60 +164,87 @@ pub mod pallet { MatchingCompleted { total_trades: u32, total_volume: Amount, - } + }, } #[pallet::error] pub enum Error { /// Order not found OrderNotFound, - + /// Not the order owner NotOrderOwner, - + /// Order not active OrderNotActive, - + /// Price must be > 0 InvalidPrice, - + /// Quantity must be > 0 InvalidQuantity, - + /// Insufficient balance InsufficientBalance, - + /// Too many pending orders this block TooManyPendingOrders, //too many user orders TooManyUserOrders, - + /// Too many pending cancellations this block TooManyPendingCancellations, - + /// Arithmetic overflow ArithmeticOverflow, - + /// Arithmetic underflow ArithmeticUnderflow, - + /// Failed to unreserve funds FailedToUnreserveFunds, - + /// No matching orders NoMatchingOrders, } - - // ======================================== // HOOKS FOR MATCHING // ======================================== #[pallet::hooks] - impl Hooks> for Pallet { - fn on_finalize(_n: BlockNumberFor) { + impl Hooks> for Pallet { + /// Calculate weight based on pending work + fn on_initialize(_n: BlockNumberFor) -> Weight { + // Count pending orders quickly + let mut total_pending = 0u32; + + // Quick count of pending bids/asks + for (_, orders) in PendingBids::::iter() { + total_pending = total_pending.saturating_add(orders.len() as u32); + } + for (_, orders) in PendingAsks::::iter() { + total_pending = total_pending.saturating_add(orders.len() as u32); + } + + let cancellations = PendingCancellations::::get().len() as u32; + + // Return worst-case weight for safety + if total_pending > 0 || cancellations > 0 { + // Use complex scenario as upper bound + ::WeightInfo::on_finalize_complex( + total_pending.min(20), + total_pending.min(20), + cancellations.min(10), + ) + } else { + ::WeightInfo::on_finalize_empty() + } + } + + // on finalize + fn on_finalize(_n: BlockNumberFor) { let mut orders_map = BTreeMap::new(); //load all orders, will need to modify for sure @@ -235,13 +256,12 @@ pub mod pallet { // These will load the temp caches //================================ - // Load pending bids let mut pending_bids = BTreeMap::new(); for (price, order_ids) in PendingBids::::iter() { pending_bids.insert(price, order_ids.into_inner()); } - + // Load pending asks let mut pending_asks = BTreeMap::new(); for (price, order_ids) in PendingAsks::::iter() { @@ -253,7 +273,7 @@ pub mod pallet { for (price, order_ids) in Bids::::iter() { persistent_bids.insert(price, order_ids.into_inner()); } - + // Load persistent asks let mut persistent_asks = BTreeMap::new(); for (price, order_ids) in Asks::::iter() { @@ -268,18 +288,18 @@ pub mod pallet { &mut persistent_asks, &mut orders_map, ); - } + } let mut all_trades: Vec> = Vec::new(); // here we are matching first only from the temp cache - let (pending_trades, unmatched) = match match_pending_internal(pending_bids, pending_asks, &mut orders_map) { - Ok(result) => result, - Err(_) => (Vec::new(), Vec::new()), - }; + let (pending_trades, unmatched) = + match match_pending_internal(pending_bids, pending_asks, &mut orders_map) { + Ok(result) => result, + Err(_) => (Vec::new(), Vec::new()), + }; all_trades.extend(pending_trades); - if !unmatched.is_empty() { let persistent_trades = match match_persistent_storage( @@ -291,248 +311,270 @@ pub mod pallet { Ok(trades) => trades, Err(_) => Vec::new(), }; - + all_trades.extend(persistent_trades); } // At this point, we have in memory done all necessary transactions // Now we need to adjust order/money management let mut total_volume = 0u128; - - for trade in all_trades.iter_mut() { - // Set trade_id - let trade_id = NextTradeId::::get(); - trade.trade_id = trade_id; - - // Transfer USDT from buyer to seller - let usdt_amount = trade.price.saturating_mul(trade.quantity); - let _ = assets::Pallet::::transfer_locked( - &trade.buyer, - &trade.seller, - USDT, - usdt_amount, - ); - - // Transfer ETH from seller to buyer - let _ = assets::Pallet::::transfer_locked( - &trade.seller, - &trade.buyer, - ETH, - trade.quantity, - ); - - // Unlock funds for both parties(NOt required i realized that transfer_locked alredy transfer - //to free balance, unlocking might unlock some other things not in the trade) - //let _ = assets::Pallet::::unlock_funds(&trade.seller, USDT, usdt_amount); - //let _ = assets::Pallet::::unlock_funds(&trade.buyer, ETH, trade.quantity); - - // Store trade - Trades::::insert(trade_id, trade.clone()); - NextTradeId::::put(trade_id + 1); - - // Emit event - Self::deposit_event(Event::TradeExecuted { - trade_id, - buy_order_id: trade.buy_order_id, - sell_order_id: trade.sell_order_id, - buyer: trade.buyer.clone(), - seller: trade.seller.clone(), - price: trade.price, - quantity: trade.quantity, - }); - - total_volume = total_volume.saturating_add(usdt_amount); - } + for trade in all_trades.iter_mut() { + // Set trade_id + let trade_id = NextTradeId::::get(); + trade.trade_id = trade_id; + + // Transfer USDT from buyer to seller + let usdt_amount = trade.price.saturating_mul(trade.quantity); + let _ = assets::Pallet::::transfer_locked( + &trade.buyer, + &trade.seller, + USDT, + usdt_amount, + ); + + // Transfer ETH from seller to buyer + let _ = assets::Pallet::::transfer_locked( + &trade.seller, + &trade.buyer, + ETH, + trade.quantity, + ); + + // Unlock funds for both parties(NOt required i realized that transfer_locked alredy transfer + //to free balance, unlocking might unlock some other things not in the trade) + //let _ = assets::Pallet::::unlock_funds(&trade.seller, USDT, usdt_amount); + //let _ = assets::Pallet::::unlock_funds(&trade.buyer, ETH, trade.quantity); + + // Store trade + Trades::::insert(trade_id, trade.clone()); + NextTradeId::::put(trade_id + 1); + + // Emit event + Self::deposit_event(Event::TradeExecuted { + trade_id, + buy_order_id: trade.buy_order_id, + sell_order_id: trade.sell_order_id, + buyer: trade.buyer.clone(), + seller: trade.seller.clone(), + price: trade.price, + quantity: trade.quantity, + }); + + total_volume = total_volume.saturating_add(usdt_amount); + } + + // Now we need to unlock funds which are cancelled + for (order_id, order) in orders_map.iter() { + if order.status == OrderStatus::Cancelled { + let remaining = order.quantity.saturating_sub(order.filled_quantity); + + if remaining > 0 { + let (asset, amount) = match order.side { + OrderSide::Buy => { + let total = order.price.saturating_mul(remaining); + (USDT, total) + } + OrderSide::Sell => (ETH, remaining), + }; + + let _ = assets::Pallet::::unlock_funds(&order.trader, asset, amount); + + Self::deposit_event(Event::OrderCancelled { + order_id: *order_id, + trader: order.trader.clone(), + }); + } + } + } + + // Emit events for filled/partially filled: + for (order_id, order) in orders_map.iter() { + Orders::::insert(order_id, order); - // Now we need to unlock funds which are cancelled - for (order_id, order) in orders_map.iter() { - if order.status == OrderStatus::Cancelled { - let remaining = order.quantity.saturating_sub(order.filled_quantity); - - if remaining > 0 { - let (asset, amount) = match order.side { - OrderSide::Buy => { - let total = order.price.saturating_mul(remaining); - (USDT, total) - }, - OrderSide::Sell => (ETH, remaining), - }; - - let _ = assets::Pallet::::unlock_funds(&order.trader, asset, amount); - - Self::deposit_event(Event::OrderCancelled { + // Emit events for filled/partially filled orders + if order.status == OrderStatus::Filled { + Self::deposit_event(Event::OrderFilled { + order_id: *order_id, + trader: order.trader.clone(), + }); + } else if order.status == OrderStatus::PartiallyFilled { + let remaining = order.quantity.saturating_sub(order.filled_quantity); + Self::deposit_event(Event::OrderPartiallyFilled { order_id: *order_id, trader: order.trader.clone(), + filled_quantity: order.filled_quantity, + remaining_quantity: remaining, }); } } - } - - // Emit events for filled/partially filled: - for (order_id, order) in orders_map.iter() { - Orders::::insert(order_id, order); - - // Emit events for filled/partially filled orders - if order.status == OrderStatus::Filled { - Self::deposit_event(Event::OrderFilled { - order_id: *order_id, - trader: order.trader.clone(), - }); - } else if order.status == OrderStatus::PartiallyFilled { - let remaining = order.quantity.saturating_sub(order.filled_quantity); - Self::deposit_event(Event::OrderPartiallyFilled { - order_id: *order_id, - trader: order.trader.clone(), - filled_quantity: order.filled_quantity, - remaining_quantity: remaining, - }); - } - } - // Here we modify the StorageDoubleMap - for (price, order_ids) in persistent_bids.iter(){ - if !order_ids.is_empty() { - match BoundedVec::::try_from(order_ids.clone()) { - Ok(bounded) => { - Bids::::insert(price, bounded); - }, - Err(_) => { - // Doing this so that its save and bounded(altho this is mostly guaranteed because its from pending asks/bids and also pendingcancellations) - let truncated: Vec = order_ids.iter() - .take(T::MaxOrders::get() as usize) - .cloned() - .collect(); - - if let Ok(bounded) = BoundedVec::::try_from(truncated.clone()) { + // Here we modify the StorageDoubleMap + for (price, order_ids) in persistent_bids.iter() { + if !order_ids.is_empty() { + match BoundedVec::::try_from(order_ids.clone()) { + Ok(bounded) => { Bids::::insert(price, bounded); } + Err(_) => { + // Doing this so that its save and bounded(altho this is mostly guaranteed because its from pending asks/bids and also pendingcancellations) + let truncated: Vec = order_ids + .iter() + .take(T::MaxOrders::get() as usize) + .cloned() + .collect(); + + if let Ok(bounded) = + BoundedVec::::try_from(truncated.clone()) + { + Bids::::insert(price, bounded); + } + } } } } - } - for (price, order_ids) in persistent_asks.iter(){ - if !order_ids.is_empty() { - match BoundedVec::::try_from(order_ids.clone()) { - Ok(bounded) => { - Asks::::insert(price, bounded); - }, - Err(_) => { - // Doing this so that its save and bounded(altho this is mostly guaranteed because its from pending asks/bids and also pendingcancellations) - let truncated: Vec = order_ids.iter() - .take(T::MaxOrders::get() as usize) - .cloned() - .collect(); - - if let Ok(bounded) = BoundedVec::::try_from(truncated.clone()) { + for (price, order_ids) in persistent_asks.iter() { + if !order_ids.is_empty() { + match BoundedVec::::try_from(order_ids.clone()) { + Ok(bounded) => { Asks::::insert(price, bounded); } + Err(_) => { + // Doing this so that its save and bounded(altho this is mostly guaranteed because its from pending asks/bids and also pendingcancellations) + let truncated: Vec = order_ids + .iter() + .take(T::MaxOrders::get() as usize) + .cloned() + .collect(); + + if let Ok(bounded) = + BoundedVec::::try_from(truncated.clone()) + { + Asks::::insert(price, bounded); + } + } } } } - } - // Clear Pending Bids and Asks - let _ = PendingBids::::clear(u32::MAX, None); - let _ = PendingAsks::::clear(u32::MAX, None); + // Clear Pending Bids and Asks + let _ = PendingBids::::clear(u32::MAX, None); + let _ = PendingAsks::::clear(u32::MAX, None); + + //remove the cancellation storage + let _ = PendingCancellations::::kill(); - //EMIT event about complete trades - Self::deposit_event(Event::MatchingCompleted { total_trades:all_trades.len() as u32, total_volume: total_volume }); + //EMIT event about complete trades + Self::deposit_event(Event::MatchingCompleted { + total_trades: all_trades.len() as u32, + total_volume: total_volume, + }); - //Ok(()) + //Ok(()) } } - // ============================================================ // EXTRINSICS // ============================================================ #[pallet::call] - impl Pallet { - + impl Pallet { /// Place a limit order #[pallet::call_index(0)] - #[pallet::weight(10000)] + #[pallet::weight(::WeightInfo::place_order())] pub fn place_order( origin: OriginFor, side: OrderSide, price: Amount, quantity: Amount, - order_type: OrderType - ) -> DispatchResult { - let trader = ensure_signed(origin)?; - ensure!(price > 0, Error::::InvalidPrice); - ensure!(quantity > 0, Error::::InvalidQuantity); - - let (asset, amount_to_lock) = match side { - OrderSide::Buy => { - let total_amount = price.checked_mul(quantity).ok_or(Error::::ArithmeticOverflow)?; - (USDT,total_amount) - }, - OrderSide::Sell => { - (ETH, quantity) - } - }; - assets::Pallet::::lock_funds(&trader, asset, amount_to_lock)?; - - let order_id = NextOrderId::::get(); - let order = Order { - order_id, - trader: trader.clone(), - side, - status: OrderStatus::Open, - order_type, - price, - quantity, - filled_quantity: 0, - ttl: None, - }; - - Orders::::insert(order_id, order); - if side == OrderSide::Buy { - PendingBids::::try_mutate(price, |orders| { - orders.try_push(order_id).map_err(|_| Error::::TooManyPendingOrders) - })?; - } else { - PendingAsks::::try_mutate(price, |orders| { - orders.try_push(order_id).map_err(|_| Error::::TooManyPendingOrders) - })?; + order_type: OrderType, + ) -> DispatchResult { + let trader = ensure_signed(origin)?; + ensure!(price > 0, Error::::InvalidPrice); + ensure!(quantity > 0, Error::::InvalidQuantity); + + let (asset, amount_to_lock) = match side { + OrderSide::Buy => { + let total_amount = price + .checked_mul(quantity) + .ok_or(Error::::ArithmeticOverflow)?; + (USDT, total_amount) } - + OrderSide::Sell => (ETH, quantity), + }; + assets::Pallet::::lock_funds(&trader, asset, amount_to_lock)?; + + let order_id = NextOrderId::::get(); + let order = Order { + order_id, + trader: trader.clone(), + side, + status: OrderStatus::Open, + order_type, + price, + quantity, + filled_quantity: 0, + ttl: None, + }; + + Orders::::insert(order_id, order); + if side == OrderSide::Buy { + PendingBids::::try_mutate(price, |orders| { + orders + .try_push(order_id) + .map_err(|_| Error::::TooManyPendingOrders) + })?; + } else { + PendingAsks::::try_mutate(price, |orders| { + orders + .try_push(order_id) + .map_err(|_| Error::::TooManyPendingOrders) + })?; + } + UserOrders::::try_mutate(trader.clone(), |orders| { - orders.try_push(order_id).map_err(|_| Error::::TooManyUserOrders) + orders + .try_push(order_id) + .map_err(|_| Error::::TooManyUserOrders) })?; NextOrderId::::put(order_id + 1); - Self::deposit_event(Event::OrderPlaced { order_id: order_id, side: side, price: price, quantity: quantity }); + Self::deposit_event(Event::OrderPlaced { + order_id: order_id, + side: side, + price: price, + quantity: quantity, + }); Ok(()) - - } + } #[pallet::call_index(1)] - #[pallet::weight(10000)] - pub fn cancel_order( - origin: OriginFor, - order_id: OrderId, - ) -> DispatchResult { + #[pallet::weight(::WeightInfo::cancel_order())] + pub fn cancel_order(origin: OriginFor, order_id: OrderId) -> DispatchResult { let trader = ensure_signed(origin)?; let order = Orders::::get(order_id).ok_or(Error::::OrderNotFound)?; ensure!(trader == order.trader, Error::::NotOrderOwner); ensure!( - order.status != OrderStatus::Filled, Error::::OrderNotActive + order.status != OrderStatus::Filled, + Error::::OrderNotActive ); PendingCancellations::::try_mutate(|cancellations| { - cancellations.try_push(order_id).map_err(|_| Error::::TooManyPendingCancellations) + cancellations + .try_push(order_id) + .map_err(|_| Error::::TooManyPendingCancellations) })?; - Self::deposit_event(Event::CancellationRequested { order_id: order.order_id, trader: trader }); + Self::deposit_event(Event::CancellationRequested { + order_id: order.order_id, + trader: trader, + }); Ok(()) } @@ -541,54 +583,54 @@ pub mod pallet { // ====================================== // Getter functions for storage/for some reason, directly acccessing them doesnt work // ======================================= - impl Pallet { + impl Pallet { pub fn next_order_id() -> OrderId { NextOrderId::::get() } - + /// Get the next trade ID pub fn next_trade_id() -> TradeId { NextTradeId::::get() } - + /// Get an order by ID pub fn get_order(order_id: OrderId) -> Option> { Orders::::get(order_id) } - + /// Get a trade by ID pub fn get_trade(trade_id: TradeId) -> Option> { Trades::::get(trade_id) } - + /// Get bids at a specific price level pub fn get_bids_at_price(price: Amount) -> Vec { Bids::::get(price).into_inner() } - + /// Get asks at a specific price level pub fn get_asks_at_price(price: Amount) -> Vec { Asks::::get(price).into_inner() } - + /// Get pending bids at a specific price level pub fn get_pending_bids_at_price(price: Amount) -> Vec { PendingBids::::get(price).into_inner() } - + /// Get pending asks at a specific price level pub fn get_pending_asks_at_price(price: Amount) -> Vec { PendingAsks::::get(price).into_inner() } - + /// Get pending cancellations pub fn get_pending_cancellations() -> Vec { PendingCancellations::::get().into_inner() } - + /// Get user's orders pub fn get_user_orders(user: &T::AccountId) -> Vec { UserOrders::::get(user).into_inner() } } -} \ No newline at end of file +} diff --git a/pallets/orderbook/src/mock.rs b/pallets/orderbook/src/mock.rs index 71273f6..db729c4 100644 --- a/pallets/orderbook/src/mock.rs +++ b/pallets/orderbook/src/mock.rs @@ -1,9 +1,8 @@ +use crate as pallet_orderbook; use frame_support::derive_impl; use frame_system::pallet; -use sp_runtime::BuildStorage; -use crate as pallet_orderbook; use sp_runtime::traits::parameter_types; - +use sp_runtime::BuildStorage; type Block = frame_system::mocking::MockBlock; @@ -21,7 +20,7 @@ mod runtime { RuntimeHoldReason, RuntimeSlashReason, RuntimeLockId, - RuntimeTask, + RuntimeTask )] pub struct Test; @@ -34,7 +33,6 @@ mod runtime { #[runtime::pallet_index(2)] pub type Orderbook = pallet_orderbook::Pallet; - } #[derive_impl(frame_system::config_preludes::TestDefaultConfig)] @@ -42,7 +40,7 @@ impl frame_system::Config for Test { type Block = Block; } -impl pallet_assets::Config for Test{ +impl pallet_assets::Config for Test { type RuntimeEvent = RuntimeEvent; type WeightInfo = (); } @@ -60,14 +58,12 @@ impl pallet_orderbook::Config for Test { type MaxCancellationOrders = MaxCancellationOrders; type MaxOrders = MaxOrders; type MaxUserOrders = MaxUserOrders; + type WeightInfo = pallet_orderbook::weights::SubstrateWeight; } - pub fn new_test_ext() -> sp_io::TestExternalities { frame_system::GenesisConfig::::default() .build_storage() .unwrap() .into() } - - diff --git a/pallets/orderbook/src/tests.rs b/pallets/orderbook/src/tests.rs index a0ec505..73d86ef 100644 --- a/pallets/orderbook/src/tests.rs +++ b/pallets/orderbook/src/tests.rs @@ -1,7 +1,7 @@ use crate::mock::*; use crate::types::*; -use frame_support::{assert_ok, assert_noop, traits::Hooks}; -use pallet_assets::{USDT, ETH}; +use frame_support::{assert_noop, assert_ok, traits::Hooks}; +use pallet_assets::{ETH, USDT}; // Simple u64 accounts for testing fn alice() -> u64 { @@ -19,18 +19,10 @@ fn charlie() -> u64 { // Helper to fund accounts fn fund_account(account: u64, usdt: u128, eth: u128) { if usdt > 0 { - assert_ok!(Assets::deposit( - RuntimeOrigin::signed(account), - USDT, - usdt - )); + assert_ok!(Assets::deposit(RuntimeOrigin::signed(account), USDT, usdt)); } if eth > 0 { - assert_ok!(Assets::deposit( - RuntimeOrigin::signed(account), - ETH, - eth - )); + assert_ok!(Assets::deposit(RuntimeOrigin::signed(account), ETH, eth)); } } @@ -43,7 +35,7 @@ fn test_place_buy_order_works() { new_test_ext().execute_with(|| { let alice = alice(); fund_account(alice, 10_000, 0); - + // Place buy order: 10 ETH @ $100 assert_ok!(Orderbook::place_order( RuntimeOrigin::signed(alice), @@ -52,7 +44,7 @@ fn test_place_buy_order_works() { 10, OrderType::Limit, )); - + // Check order was created let order = Orderbook::get_order(0).expect("Order should exist"); assert_eq!(order.trader, alice); @@ -60,14 +52,14 @@ fn test_place_buy_order_works() { assert_eq!(order.price, 100); assert_eq!(order.quantity, 10); assert_eq!(order.status, OrderStatus::Open); - + // Check order ID incremented assert_eq!(Orderbook::next_order_id(), 1); - + // Check funds were locked (10 ETH * $100 = 1000 USDT) assert_eq!(Assets::get_free_balance(&alice, USDT), 9_000); assert_eq!(Assets::get_locked_balance(&alice, USDT), 1_000); - + // Check order was added to pending bids let pending = Orderbook::get_pending_bids_at_price(100); assert_eq!(pending.len(), 1); @@ -80,7 +72,7 @@ fn test_place_sell_order_works() { new_test_ext().execute_with(|| { let alice = alice(); fund_account(alice, 0, 100); - + // Place sell order: 10 ETH @ $100 assert_ok!(Orderbook::place_order( RuntimeOrigin::signed(alice), @@ -89,16 +81,16 @@ fn test_place_sell_order_works() { 10, OrderType::Limit, )); - + // Check order was created let order = Orderbook::get_order(0).expect("Order should exist"); assert_eq!(order.side, OrderSide::Sell); assert_eq!(order.price, 100); - + // Check funds were locked assert_eq!(Assets::get_free_balance(&alice, ETH), 90); assert_eq!(Assets::get_locked_balance(&alice, ETH), 10); - + // Check order was added to pending asks let pending = Orderbook::get_pending_asks_at_price(100); assert_eq!(pending.len(), 1); @@ -113,7 +105,7 @@ fn test_place_multiple_orders_same_price() { let bob = bob(); fund_account(alice, 10_000, 0); fund_account(bob, 10_000, 0); - + // Alice places buy order assert_ok!(Orderbook::place_order( RuntimeOrigin::signed(alice), @@ -122,7 +114,7 @@ fn test_place_multiple_orders_same_price() { 5, OrderType::Limit, )); - + // Bob places buy order at same price assert_ok!(Orderbook::place_order( RuntimeOrigin::signed(bob), @@ -131,13 +123,13 @@ fn test_place_multiple_orders_same_price() { 10, OrderType::Limit, )); - + // Check both orders in pending bids let pending = Orderbook::get_pending_bids_at_price(100); assert_eq!(pending.len(), 2); assert_eq!(pending[0], 0); // Alice first (FIFO) assert_eq!(pending[1], 1); // Bob second - + // Check next order ID assert_eq!(Orderbook::next_order_id(), 2); }); @@ -148,16 +140,16 @@ fn test_place_market_order() { new_test_ext().execute_with(|| { let alice = alice(); fund_account(alice, 10_000, 0); - + // Place market buy order (price is ignored) assert_ok!(Orderbook::place_order( RuntimeOrigin::signed(alice), OrderSide::Buy, - 200, + 200, 10, OrderType::Market, )); - + let order = Orderbook::get_order(0).expect("Order should exist"); assert_eq!(order.order_type, OrderType::Market); }); @@ -172,7 +164,7 @@ fn test_place_order_invalid_price_fails() { new_test_ext().execute_with(|| { let alice = alice(); fund_account(alice, 10_000, 0); - + assert_noop!( Orderbook::place_order( RuntimeOrigin::signed(alice), @@ -191,7 +183,7 @@ fn test_place_order_invalid_quantity_fails() { new_test_ext().execute_with(|| { let alice = alice(); fund_account(alice, 10_000, 0); - + assert_noop!( Orderbook::place_order( RuntimeOrigin::signed(alice), @@ -210,7 +202,7 @@ fn test_place_order_insufficient_balance_fails() { new_test_ext().execute_with(|| { let alice = alice(); fund_account(alice, 500, 0); // Only 500 USDT - + // Try to buy 10 ETH @ $100 (needs 1000 USDT) assert_noop!( Orderbook::place_order( @@ -230,7 +222,7 @@ fn test_place_order_arithmetic_overflow() { new_test_ext().execute_with(|| { let alice = alice(); fund_account(alice, u128::MAX, 0); - + // Try to create order that would overflow assert_noop!( Orderbook::place_order( @@ -254,7 +246,7 @@ fn test_cancel_order_works() { new_test_ext().execute_with(|| { let alice = alice(); fund_account(alice, 10_000, 0); - + // Place order assert_ok!(Orderbook::place_order( RuntimeOrigin::signed(alice), @@ -263,13 +255,13 @@ fn test_cancel_order_works() { 10, OrderType::Limit, )); - + // Cancel order assert_ok!(Orderbook::cancel_order( RuntimeOrigin::signed(alice), 0, // order_id )); - + // Check cancellation was queued let cancellations = Orderbook::get_pending_cancellations(); assert_eq!(cancellations.len(), 1); @@ -283,7 +275,7 @@ fn test_cancel_order_not_owner_fails() { let alice = alice(); let bob = bob(); fund_account(alice, 10_000, 0); - + // Alice places order assert_ok!(Orderbook::place_order( RuntimeOrigin::signed(alice), @@ -292,13 +284,10 @@ fn test_cancel_order_not_owner_fails() { 10, OrderType::Limit, )); - + // Bob tries to cancel Alice's order - should fail assert_noop!( - Orderbook::cancel_order( - RuntimeOrigin::signed(bob), - 0, - ), + Orderbook::cancel_order(RuntimeOrigin::signed(bob), 0,), crate::Error::::NotOrderOwner ); }); @@ -308,7 +297,7 @@ fn test_cancel_order_not_owner_fails() { fn test_cancel_nonexistent_order_fails() { new_test_ext().execute_with(|| { let alice = alice(); - + assert_noop!( Orderbook::cancel_order( RuntimeOrigin::signed(alice), @@ -324,7 +313,7 @@ fn test_cancel_multiple_orders() { new_test_ext().execute_with(|| { let alice = alice(); fund_account(alice, 10_000, 0); - + // Place 3 orders for i in 0..3 { assert_ok!(Orderbook::place_order( @@ -335,11 +324,11 @@ fn test_cancel_multiple_orders() { OrderType::Limit, )); } - + // Cancel first and third order assert_ok!(Orderbook::cancel_order(RuntimeOrigin::signed(alice), 0)); assert_ok!(Orderbook::cancel_order(RuntimeOrigin::signed(alice), 2)); - + // Check both cancellations queued let cancellations = Orderbook::get_pending_cancellations(); assert_eq!(cancellations.len(), 2); @@ -357,7 +346,7 @@ fn test_place_order_with_exact_balance() { new_test_ext().execute_with(|| { let alice = alice(); fund_account(alice, 1_000, 0); // Exactly 1000 USDT - + // Buy exactly what we can afford assert_ok!(Orderbook::place_order( RuntimeOrigin::signed(alice), @@ -366,7 +355,7 @@ fn test_place_order_with_exact_balance() { 10, // Exactly 1000 USDT needed OrderType::Limit, )); - + // Should have locked all funds assert_eq!(Assets::get_free_balance(&alice, USDT), 0); assert_eq!(Assets::get_locked_balance(&alice, USDT), 1_000); @@ -378,7 +367,7 @@ fn test_place_order_one_wei_short_fails() { new_test_ext().execute_with(|| { let alice = alice(); fund_account(alice, 999, 0); // One less than needed - + assert_noop!( Orderbook::place_order( RuntimeOrigin::signed(alice), @@ -397,20 +386,24 @@ fn test_sequential_orders_increment_ids() { new_test_ext().execute_with(|| { let alice = alice(); fund_account(alice, 100_000, 1000); - + // Place 5 orders for i in 0..5 { assert_ok!(Orderbook::place_order( RuntimeOrigin::signed(alice), - if i % 2 == 0 { OrderSide::Buy } else { OrderSide::Sell }, + if i % 2 == 0 { + OrderSide::Buy + } else { + OrderSide::Sell + }, 100, 1, OrderType::Limit, )); - + // Check ID incremented correctly assert_eq!(Orderbook::next_order_id(), i + 1); - + // Check order exists with correct ID let order = Orderbook::get_order(i).expect("Order should exist"); assert_eq!(order.order_id, i); @@ -423,16 +416,16 @@ fn test_large_order_values() { new_test_ext().execute_with(|| { let alice = alice(); fund_account(alice, 1_000_000_000, 0); // 1 billion USDT - + // Place large order assert_ok!(Orderbook::place_order( RuntimeOrigin::signed(alice), OrderSide::Buy, - 10_000, // $10,000 per ETH + 10_000, // $10,000 per ETH 100_000, // 100k ETH OrderType::Limit, )); - + // Check huge amount locked (10k * 100k = 1 billion) assert_eq!(Assets::get_locked_balance(&alice, USDT), 1_000_000_000); }); @@ -444,11 +437,11 @@ fn test_different_users_different_orders() { let alice = alice(); let bob = bob(); let charlie = charlie(); - + fund_account(alice, 10_000, 0); fund_account(bob, 0, 100); fund_account(charlie, 5_000, 50); - + // Each user places different order assert_ok!(Orderbook::place_order( RuntimeOrigin::signed(alice), @@ -457,7 +450,7 @@ fn test_different_users_different_orders() { 10, OrderType::Limit, )); - + assert_ok!(Orderbook::place_order( RuntimeOrigin::signed(bob), OrderSide::Sell, @@ -465,7 +458,7 @@ fn test_different_users_different_orders() { 20, OrderType::Limit, )); - + assert_ok!(Orderbook::place_order( RuntimeOrigin::signed(charlie), OrderSide::Buy, @@ -473,28 +466,27 @@ fn test_different_users_different_orders() { 5, OrderType::Limit, )); - + // Verify each order has correct owner assert_eq!(Orderbook::get_order(0).unwrap().trader, alice); assert_eq!(Orderbook::get_order(1).unwrap().trader, bob); assert_eq!(Orderbook::get_order(2).unwrap().trader, charlie); - + // Verify 3 orders created assert_eq!(Orderbook::next_order_id(), 3); }); } - #[test] fn test_simple_buy_sell_match() { new_test_ext().execute_with(|| { let alice = alice(); let bob = bob(); - + // Setup: Give Alice USDT, Bob ETH fund_account(alice, 10_000, 0); fund_account(bob, 0, 100); - + // Alice: Buy 10 ETH @ $100 assert_ok!(Orderbook::place_order( RuntimeOrigin::signed(alice), @@ -503,7 +495,7 @@ fn test_simple_buy_sell_match() { 10, OrderType::Limit, )); - + // Bob: Sell 10 ETH @ $100 assert_ok!(Orderbook::place_order( RuntimeOrigin::signed(bob), @@ -512,44 +504,44 @@ fn test_simple_buy_sell_match() { 10, OrderType::Limit, )); - + // Both orders pending assert_eq!(Orderbook::get_pending_bids_at_price(100).len(), 1); assert_eq!(Orderbook::get_pending_asks_at_price(100).len(), 1); - + // Trigger matching by advancing to next block System::set_block_number(1); Orderbook::on_finalize(1); - + // Verify trade executed let trade = Orderbook::get_trade(0).expect("Trade should exist"); assert_eq!(trade.buyer, alice); assert_eq!(trade.seller, bob); assert_eq!(trade.price, 100); assert_eq!(trade.quantity, 10); - + // Verify balances after settlement // Alice: spent 1000 USDT, got 10 ETH assert_eq!(Assets::get_free_balance(&alice, USDT), 9_000); assert_eq!(Assets::get_free_balance(&alice, ETH), 10); assert_eq!(Assets::get_locked_balance(&alice, USDT), 0); assert_eq!(Assets::get_locked_balance(&alice, ETH), 0); - + // Bob: got 1000 USDT, spent 10 ETH assert_eq!(Assets::get_free_balance(&bob, USDT), 1_000); assert_eq!(Assets::get_free_balance(&bob, ETH), 90); assert_eq!(Assets::get_locked_balance(&bob, USDT), 0); assert_eq!(Assets::get_locked_balance(&bob, ETH), 0); - + // Verify orders are filled let alice_order = Orderbook::get_order(0).unwrap(); assert_eq!(alice_order.status, OrderStatus::Filled); assert_eq!(alice_order.filled_quantity, 10); - + let bob_order = Orderbook::get_order(1).unwrap(); assert_eq!(bob_order.status, OrderStatus::Filled); assert_eq!(bob_order.filled_quantity, 10); - + // Verify trade ID incremented assert_eq!(Orderbook::next_trade_id(), 1); }); @@ -561,14 +553,14 @@ fn test_partial_fill_matching_debug() { new_test_ext().execute_with(|| { let alice = alice(); let bob = bob(); - + fund_account(alice, 10_000, 0); fund_account(bob, 0, 100); - + println!("=== Initial state ==="); println!("Alice USDT: {}", Assets::get_free_balance(&alice, USDT)); println!("Bob ETH: {}", Assets::get_free_balance(&bob, ETH)); - + // Alice: Buy 10 ETH @ $100 assert_ok!(Orderbook::place_order( RuntimeOrigin::signed(alice), @@ -577,11 +569,17 @@ fn test_partial_fill_matching_debug() { 10, OrderType::Limit, )); - + println!("\n=== After Alice order ==="); - println!("Alice free USDT: {}", Assets::get_free_balance(&alice, USDT)); - println!("Alice locked USDT: {}", Assets::get_locked_balance(&alice, USDT)); - + println!( + "Alice free USDT: {}", + Assets::get_free_balance(&alice, USDT) + ); + println!( + "Alice locked USDT: {}", + Assets::get_locked_balance(&alice, USDT) + ); + // Bob: Sell only 5 ETH @ $100 assert_ok!(Orderbook::place_order( RuntimeOrigin::signed(bob), @@ -590,33 +588,51 @@ fn test_partial_fill_matching_debug() { 5, OrderType::Limit, )); - + println!("\n=== After Bob order ==="); println!("Bob free ETH: {}", Assets::get_free_balance(&bob, ETH)); println!("Bob locked ETH: {}", Assets::get_locked_balance(&bob, ETH)); - + >::on_finalize(1); - + println!("\n=== After matching ==="); let alice_order = Orderbook::get_order(0).unwrap(); println!("Alice order status: {:?}", alice_order.status); - println!("Alice filled: {}/{}", alice_order.filled_quantity, alice_order.quantity); - + println!( + "Alice filled: {}/{}", + alice_order.filled_quantity, alice_order.quantity + ); + let bob_order = Orderbook::get_order(1).unwrap(); println!("Bob order status: {:?}", bob_order.status); - println!("Bob filled: {}/{}", bob_order.filled_quantity, bob_order.quantity); - + println!( + "Bob filled: {}/{}", + bob_order.filled_quantity, bob_order.quantity + ); + println!("\n=== Final balances ==="); - println!("Alice free USDT: {}", Assets::get_free_balance(&alice, USDT)); - println!("Alice locked USDT: {}", Assets::get_locked_balance(&alice, USDT)); + println!( + "Alice free USDT: {}", + Assets::get_free_balance(&alice, USDT) + ); + println!( + "Alice locked USDT: {}", + Assets::get_locked_balance(&alice, USDT) + ); println!("Alice free ETH: {}", Assets::get_free_balance(&alice, ETH)); - println!("Alice locked ETH: {}", Assets::get_locked_balance(&alice, ETH)); - + println!( + "Alice locked ETH: {}", + Assets::get_locked_balance(&alice, ETH) + ); + println!("Bob free USDT: {}", Assets::get_free_balance(&bob, USDT)); - println!("Bob locked USDT: {}", Assets::get_locked_balance(&bob, USDT)); + println!( + "Bob locked USDT: {}", + Assets::get_locked_balance(&bob, USDT) + ); println!("Bob free ETH: {}", Assets::get_free_balance(&bob, ETH)); println!("Bob locked ETH: {}", Assets::get_locked_balance(&bob, ETH)); - + let trade = Orderbook::get_trade(0).unwrap(); println!("\nTrade: {} ETH @ ${}", trade.quantity, trade.price); }); @@ -627,11 +643,11 @@ fn test_price_time_priority_fifo() { let alice = alice(); let bob = bob(); let charlie = charlie(); - + fund_account(alice, 0, 100); fund_account(bob, 0, 100); fund_account(charlie, 10_000, 0); - + // Alice: Sell 5 ETH @ $100 (FIRST) assert_ok!(Orderbook::place_order( RuntimeOrigin::signed(alice), @@ -640,7 +656,7 @@ fn test_price_time_priority_fifo() { 5, OrderType::Limit, )); - + // Bob: Sell 5 ETH @ $100 (SECOND - same price, later time) assert_ok!(Orderbook::place_order( RuntimeOrigin::signed(bob), @@ -649,7 +665,7 @@ fn test_price_time_priority_fifo() { 5, OrderType::Limit, )); - + // Charlie: Buy 5 ETH @ $100 assert_ok!(Orderbook::place_order( RuntimeOrigin::signed(charlie), @@ -658,27 +674,27 @@ fn test_price_time_priority_fifo() { 5, OrderType::Limit, )); - + // Trigger matching System::set_block_number(1); Orderbook::on_finalize(1); - + // Should match with Alice (FIFO - first in, first out) let trade = Orderbook::get_trade(0).unwrap(); assert_eq!(trade.seller, alice); // Alice matched, not Bob assert_eq!(trade.buyer, charlie); - + // Alice's order filled, Bob's still open let alice_order = Orderbook::get_order(0).unwrap(); assert_eq!(alice_order.status, OrderStatus::Filled); - + let bob_order = Orderbook::get_order(1).unwrap(); assert_eq!(bob_order.status, OrderStatus::Open); // Still waiting! - + // Verify Alice got paid assert_eq!(Assets::get_free_balance(&alice, USDT), 500); assert_eq!(Assets::get_free_balance(&alice, ETH), 95); - + // Bob didn't trade yet assert_eq!(Assets::get_free_balance(&bob, USDT), 0); assert_eq!(Assets::get_locked_balance(&bob, ETH), 5); // Still locked @@ -690,10 +706,10 @@ fn test_no_match_price_spread() { new_test_ext().execute_with(|| { let alice = alice(); let bob = bob(); - + fund_account(alice, 10_000, 0); fund_account(bob, 0, 100); - + // Alice: Buy @ $95 assert_ok!(Orderbook::place_order( RuntimeOrigin::signed(alice), @@ -702,7 +718,7 @@ fn test_no_match_price_spread() { 10, OrderType::Limit, )); - + // Bob: Sell @ $105 (no match - spread too wide) assert_ok!(Orderbook::place_order( RuntimeOrigin::signed(bob), @@ -711,23 +727,23 @@ fn test_no_match_price_spread() { 10, OrderType::Limit, )); - + // Trigger matching System::set_block_number(1); Orderbook::on_finalize(1); - + // No trades should execute assert!(Orderbook::get_trade(0).is_none()); - + // Both orders should remain open let alice_order = Orderbook::get_order(0).unwrap(); assert_eq!(alice_order.status, OrderStatus::Open); assert_eq!(alice_order.filled_quantity, 0); - + let bob_order = Orderbook::get_order(1).unwrap(); assert_eq!(bob_order.status, OrderStatus::Open); assert_eq!(bob_order.filled_quantity, 0); - + // Funds still locked assert_eq!(Assets::get_locked_balance(&alice, USDT), 950); assert_eq!(Assets::get_locked_balance(&bob, ETH), 10); @@ -740,11 +756,11 @@ fn test_multiple_trades_same_block() { let alice = alice(); let bob = bob(); let charlie = charlie(); - + fund_account(alice, 10_000, 0); fund_account(bob, 0, 50); fund_account(charlie, 0, 50); - + // Alice: Buy 20 ETH @ $100 assert_ok!(Orderbook::place_order( RuntimeOrigin::signed(alice), @@ -753,7 +769,7 @@ fn test_multiple_trades_same_block() { 20, OrderType::Limit, )); - + // Bob: Sell 10 ETH @ $100 assert_ok!(Orderbook::place_order( RuntimeOrigin::signed(bob), @@ -762,7 +778,7 @@ fn test_multiple_trades_same_block() { 10, OrderType::Limit, )); - + // Charlie: Sell 10 ETH @ $100 assert_ok!(Orderbook::place_order( RuntimeOrigin::signed(charlie), @@ -771,39 +787,39 @@ fn test_multiple_trades_same_block() { 10, OrderType::Limit, )); - + // Trigger matching System::set_block_number(1); Orderbook::on_finalize(1); - + // Should create 2 trades (Alice with Bob, Alice with Charlie) assert!(Orderbook::get_trade(0).is_some()); assert!(Orderbook::get_trade(1).is_some()); - + let trade1 = Orderbook::get_trade(0).unwrap(); let trade2 = Orderbook::get_trade(1).unwrap(); - + // Both trades with Alice as buyer assert_eq!(trade1.buyer, alice); assert_eq!(trade2.buyer, alice); - + // Bob and Charlie as sellers assert!(trade1.seller == bob || trade2.seller == bob); assert!(trade1.seller == charlie || trade2.seller == charlie); - + // Alice's order should be fully filled (20 ETH total) let alice_order = Orderbook::get_order(0).unwrap(); assert_eq!(alice_order.status, OrderStatus::Filled); assert_eq!(alice_order.filled_quantity, 20); - + // Alice should have 20 ETH, spent 2000 USDT assert_eq!(Assets::get_free_balance(&alice, ETH), 20); assert_eq!(Assets::get_free_balance(&alice, USDT), 8_000); assert_eq!(Assets::get_locked_balance(&alice, USDT), 0); - + // Bob got 1000 USDT assert_eq!(Assets::get_free_balance(&bob, USDT), 1_000); - + // Charlie got 1000 USDT assert_eq!(Assets::get_free_balance(&charlie, USDT), 1_000); }); @@ -814,7 +830,7 @@ fn test_cancellation_unlocks_funds() { new_test_ext().execute_with(|| { let alice = alice(); fund_account(alice, 10_000, 0); - + // Place order assert_ok!(Orderbook::place_order( RuntimeOrigin::signed(alice), @@ -823,24 +839,21 @@ fn test_cancellation_unlocks_funds() { 10, OrderType::Limit, )); - + // Verify funds locked assert_eq!(Assets::get_locked_balance(&alice, USDT), 1_000); - + // Cancel order - assert_ok!(Orderbook::cancel_order( - RuntimeOrigin::signed(alice), - 0, - )); - + assert_ok!(Orderbook::cancel_order(RuntimeOrigin::signed(alice), 0,)); + // Trigger finalization to process cancellation System::set_block_number(1); Orderbook::on_finalize(1); - + // Verify funds unlocked assert_eq!(Assets::get_free_balance(&alice, USDT), 10_000); assert_eq!(Assets::get_locked_balance(&alice, USDT), 0); - + // Verify order cancelled let order = Orderbook::get_order(0).unwrap(); assert_eq!(order.status, OrderStatus::Cancelled); @@ -853,10 +866,10 @@ fn test_market_order_matches_best_price() { new_test_ext().execute_with(|| { let alice = alice(); let bob = bob(); - + fund_account(alice, 0, 100); fund_account(bob, 10_000, 0); - + assert_ok!(Orderbook::place_order( RuntimeOrigin::signed(alice), OrderSide::Sell, @@ -864,21 +877,21 @@ fn test_market_order_matches_best_price() { 10, OrderType::Limit, )); - + // For batch matching, market orders still use the price for locking funds assert_ok!(Orderbook::place_order( RuntimeOrigin::signed(bob), OrderSide::Buy, - 95, // Match at same price + 95, // Match at same price 10, OrderType::Market, )); - + >::on_finalize(1); - + let trade = Orderbook::get_trade(0).unwrap(); assert_eq!(trade.price, 95); - + assert_eq!(Assets::get_free_balance(&bob, USDT), 9_050); assert_eq!(Assets::get_free_balance(&bob, ETH), 10); }); @@ -888,14 +901,14 @@ fn test_simple_buy_sell_match_debug() { new_test_ext().execute_with(|| { let alice = alice(); let bob = bob(); - + fund_account(alice, 10_000, 0); fund_account(bob, 0, 100); - + println!("=== Before orders ==="); println!("Alice USDT: {}", Assets::get_free_balance(&alice, USDT)); println!("Bob ETH: {}", Assets::get_free_balance(&bob, ETH)); - + assert_ok!(Orderbook::place_order( RuntimeOrigin::signed(alice), OrderSide::Buy, @@ -903,7 +916,7 @@ fn test_simple_buy_sell_match_debug() { 10, OrderType::Limit, )); - + assert_ok!(Orderbook::place_order( RuntimeOrigin::signed(bob), OrderSide::Sell, @@ -911,17 +924,23 @@ fn test_simple_buy_sell_match_debug() { 10, OrderType::Limit, )); - + println!("=== After orders placed ==="); - println!("Pending bids at 100: {:?}", Orderbook::get_pending_bids_at_price(100)); - println!("Pending asks at 100: {:?}", Orderbook::get_pending_asks_at_price(100)); + println!( + "Pending bids at 100: {:?}", + Orderbook::get_pending_bids_at_price(100) + ); + println!( + "Pending asks at 100: {:?}", + Orderbook::get_pending_asks_at_price(100) + ); println!("Order 0: {:?}", Orderbook::get_order(0)); println!("Order 1: {:?}", Orderbook::get_order(1)); - + // Call on_finalize println!("=== Calling on_finalize ==="); >::on_finalize(1); - + println!("=== After on_finalize ==="); println!("Order 0: {:?}", Orderbook::get_order(0)); println!("Order 1: {:?}", Orderbook::get_order(1)); @@ -931,4 +950,4 @@ fn test_simple_buy_sell_match_debug() { println!("Bob USDT: {}", Assets::get_free_balance(&bob, USDT)); println!("Bob ETH: {}", Assets::get_free_balance(&bob, ETH)); }); -} \ No newline at end of file +} diff --git a/pallets/orderbook/src/types.rs b/pallets/orderbook/src/types.rs index ca60845..813c43e 100644 --- a/pallets/orderbook/src/types.rs +++ b/pallets/orderbook/src/types.rs @@ -1,15 +1,26 @@ use codec::{Decode, Encode, MaxEncodedLen}; -use scale_info::TypeInfo; +use frame_support::pallet_prelude::*; use frame_support::sp_runtime::RuntimeDebug; use frame_system::*; -use frame_support::pallet_prelude::*; +use scale_info::TypeInfo; -#[derive(Encode,Decode, Clone,Copy,RuntimeDebug, PartialEq, Eq, TypeInfo, MaxEncodedLen,DecodeWithMemTracking)] +#[derive( + Encode, + Decode, + Clone, + Copy, + RuntimeDebug, + PartialEq, + Eq, + TypeInfo, + MaxEncodedLen, + DecodeWithMemTracking, +)] pub enum OrderSide { Buy, Sell, } -#[derive(Encode,Decode, Clone ,Copy,RuntimeDebug, PartialEq, Eq, TypeInfo, MaxEncodedLen)] +#[derive(Encode, Decode, Clone, Copy, RuntimeDebug, PartialEq, Eq, TypeInfo, MaxEncodedLen)] pub enum OrderStatus { Filled, PartiallyFilled, @@ -18,8 +29,19 @@ pub enum OrderStatus { Open, } -#[derive(Encode,Decode, Clone,Copy,RuntimeDebug, PartialEq, Eq, TypeInfo,MaxEncodedLen,DecodeWithMemTracking)] -pub enum OrderType{ +#[derive( + Encode, + Decode, + Clone, + Copy, + RuntimeDebug, + PartialEq, + Eq, + TypeInfo, + MaxEncodedLen, + DecodeWithMemTracking, +)] +pub enum OrderType { Market, Limit, // will add the other stuff like IOK, Stop etc later @@ -27,14 +49,14 @@ pub enum OrderType{ #[derive(Encode, Decode, Clone, Copy, PartialEq, Eq, RuntimeDebug, TypeInfo, MaxEncodedLen)] -pub struct MarketPair{ - pub base_asset : AssetId, // btc/usdt pair - pub quote_asset : AssetId, +pub struct MarketPair { + pub base_asset: AssetId, // btc/usdt pair + pub quote_asset: AssetId, } -#[derive(Encode,Decode, Clone,RuntimeDebug, PartialEq, Eq, TypeInfo,MaxEncodedLen)] +#[derive(Encode, Decode, Clone, RuntimeDebug, PartialEq, Eq, TypeInfo, MaxEncodedLen)] #[scale_info(skip_type_params(T))] -pub struct Order{ +pub struct Order { pub order_id: OrderId, pub trader: T::AccountId, pub side: OrderSide, @@ -46,7 +68,7 @@ pub struct Order{ pub ttl: Option, } -#[derive(Encode,Decode, Clone,RuntimeDebug, PartialEq, Eq, TypeInfo,MaxEncodedLen)] +#[derive(Encode, Decode, Clone, RuntimeDebug, PartialEq, Eq, TypeInfo, MaxEncodedLen)] #[scale_info(skip_type_params(T))] pub struct Trade { pub trade_id: TradeId, @@ -61,4 +83,4 @@ pub struct Trade { pub type OrderId = u64; pub type TradeId = u64; pub type AssetId = u32; -pub type Amount = u128; \ No newline at end of file +pub type Amount = u128; diff --git a/pallets/orderbook/src/weights.rs b/pallets/orderbook/src/weights.rs new file mode 100644 index 0000000..deb9749 --- /dev/null +++ b/pallets/orderbook/src/weights.rs @@ -0,0 +1,550 @@ + +//! Autogenerated weights for `pallet_orderbook` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 49.0.0 +//! DATE: 2025-10-16, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `Randalls-MacBook-Air.local`, CPU: `` +//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: `1024` + +// Executed Command: +// frame-omni-bencher +// v1 +// benchmark +// pallet +// --runtime +// target/release/wbuild/solochain-template-runtime/solochain_template_runtime.compact.compressed.wasm +// --pallet +// pallet_orderbook +// --extrinsic +// * +// --template +// ./pallets/benchmarking/frame-umbrella-weight-template.hbs +// --output +// ./pallets/orderbook/src/weights.rs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +//use frame::weights_prelude::*; +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use core::marker::PhantomData; + +/// Weight functions needed for `pallet_orderbook`. +pub trait WeightInfo { + fn place_order() -> Weight; + fn cancel_order() -> Weight; + fn on_finalize_empty() -> Weight; + fn on_finalize_with_matches(b: u32, a: u32, ) -> Weight; + fn on_finalize_no_matches(b: u32, a: u32, ) -> Weight; + fn on_finalize_with_cancellations(c: u32, ) -> Weight; + fn on_finalize_persistent_matching(p: u32, n: u32, ) -> Weight; + fn on_finalize_complex(m: u32, n: u32, c: u32, ) -> Weight; +} + +/// Weights for `pallet_orderbook` using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + /// Storage: `Assets::FreeBalance` (r:1 w:1) + /// Proof: `Assets::FreeBalance` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Assets::LockedBalance` (r:1 w:1) + /// Proof: `Assets::LockedBalance` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::NextOrderId` (r:1 w:1) + /// Proof: `Orderbook::NextOrderId` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::PendingBids` (r:1 w:1) + /// Proof: `Orderbook::PendingBids` (`max_values`: None, `max_size`: Some(8034), added: 10509, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::UserOrders` (r:1 w:1) + /// Proof: `Orderbook::UserOrders` (`max_values`: None, `max_size`: Some(8050), added: 10525, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::Orders` (r:0 w:1) + /// Proof: `Orderbook::Orders` (`max_values`: None, `max_size`: Some(120), added: 2595, mode: `MaxEncodedLen`) + fn place_order() -> Weight { + // Proof Size summary in bytes: + // Measured: `159` + // Estimated: `11515` + // Minimum execution time: 27_000_000 picoseconds. + Weight::from_parts(28_000_000, 11515) + .saturating_add(T::DbWeight::get().reads(5_u64)) + .saturating_add(T::DbWeight::get().writes(6_u64)) + } + /// Storage: `Orderbook::Orders` (r:1 w:0) + /// Proof: `Orderbook::Orders` (`max_values`: None, `max_size`: Some(120), added: 2595, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::PendingCancellations` (r:1 w:1) + /// Proof: `Orderbook::PendingCancellations` (`max_values`: Some(1), `max_size`: Some(401), added: 896, mode: `MaxEncodedLen`) + fn cancel_order() -> Weight { + // Proof Size summary in bytes: + // Measured: `289` + // Estimated: `3585` + // Minimum execution time: 15_000_000 picoseconds. + Weight::from_parts(15_000_000, 3585) + .saturating_add(T::DbWeight::get().reads(2_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `Orderbook::Orders` (r:1 w:0) + /// Proof: `Orderbook::Orders` (`max_values`: None, `max_size`: Some(120), added: 2595, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::PendingBids` (r:1 w:0) + /// Proof: `Orderbook::PendingBids` (`max_values`: None, `max_size`: Some(8034), added: 10509, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::PendingAsks` (r:1 w:0) + /// Proof: `Orderbook::PendingAsks` (`max_values`: None, `max_size`: Some(8034), added: 10509, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::Bids` (r:1 w:0) + /// Proof: `Orderbook::Bids` (`max_values`: None, `max_size`: Some(80034), added: 82509, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::Asks` (r:1 w:0) + /// Proof: `Orderbook::Asks` (`max_values`: None, `max_size`: Some(80034), added: 82509, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::PendingCancellations` (r:1 w:1) + /// Proof: `Orderbook::PendingCancellations` (`max_values`: Some(1), `max_size`: Some(401), added: 896, mode: `MaxEncodedLen`) + fn on_finalize_empty() -> Weight { + // Proof Size summary in bytes: + // Measured: `6` + // Estimated: `83499` + // Minimum execution time: 23_000_000 picoseconds. + Weight::from_parts(24_000_000, 83499) + .saturating_add(T::DbWeight::get().reads(6_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: `Orderbook::Orders` (r:101 w:100) + /// Proof: `Orderbook::Orders` (`max_values`: None, `max_size`: Some(120), added: 2595, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::PendingBids` (r:2 w:1) + /// Proof: `Orderbook::PendingBids` (`max_values`: None, `max_size`: Some(8034), added: 10509, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::PendingAsks` (r:2 w:1) + /// Proof: `Orderbook::PendingAsks` (`max_values`: None, `max_size`: Some(8034), added: 10509, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::Bids` (r:1 w:0) + /// Proof: `Orderbook::Bids` (`max_values`: None, `max_size`: Some(80034), added: 82509, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::Asks` (r:1 w:0) + /// Proof: `Orderbook::Asks` (`max_values`: None, `max_size`: Some(80034), added: 82509, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::PendingCancellations` (r:1 w:1) + /// Proof: `Orderbook::PendingCancellations` (`max_values`: Some(1), `max_size`: Some(401), added: 896, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::NextTradeId` (r:1 w:1) + /// Proof: `Orderbook::NextTradeId` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Assets::LockedBalance` (r:100 w:100) + /// Proof: `Assets::LockedBalance` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Assets::FreeBalance` (r:100 w:100) + /// Proof: `Assets::FreeBalance` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::Trades` (r:0 w:50) + /// Proof: `Orderbook::Trades` (`max_values`: None, `max_size`: Some(144), added: 2619, mode: `MaxEncodedLen`) + /// The range of component `b` is `[1, 50]`. + /// The range of component `a` is `[1, 50]`. + fn on_finalize_with_matches(b: u32, a: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0 + a * (509 ยฑ0) + b * (510 ยฑ0)` + // Estimated: `83499 + a * (2595 ยฑ0) + b * (2595 ยฑ0)` + // Minimum execution time: 378_000_000 picoseconds. + Weight::from_parts(380_000_000, 83499) + // Standard Error: 455_618 + .saturating_add(Weight::from_parts(14_754_607, 0).saturating_mul(b.into())) + // Standard Error: 455_618 + .saturating_add(Weight::from_parts(13_465_412, 0).saturating_mul(a.into())) + .saturating_add(T::DbWeight::get().reads(64_u64)) + .saturating_add(T::DbWeight::get().reads((2_u64).saturating_mul(b.into()))) + .saturating_add(T::DbWeight::get().reads((2_u64).saturating_mul(a.into()))) + .saturating_add(T::DbWeight::get().writes(61_u64)) + .saturating_add(T::DbWeight::get().writes((2_u64).saturating_mul(b.into()))) + .saturating_add(T::DbWeight::get().writes((2_u64).saturating_mul(a.into()))) + .saturating_add(Weight::from_parts(0, 2595).saturating_mul(a.into())) + .saturating_add(Weight::from_parts(0, 2595).saturating_mul(b.into())) + } + /// Storage: `Orderbook::Orders` (r:101 w:100) + /// Proof: `Orderbook::Orders` (`max_values`: None, `max_size`: Some(120), added: 2595, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::PendingBids` (r:2 w:1) + /// Proof: `Orderbook::PendingBids` (`max_values`: None, `max_size`: Some(8034), added: 10509, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::PendingAsks` (r:2 w:1) + /// Proof: `Orderbook::PendingAsks` (`max_values`: None, `max_size`: Some(8034), added: 10509, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::Bids` (r:1 w:1) + /// Proof: `Orderbook::Bids` (`max_values`: None, `max_size`: Some(80034), added: 82509, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::Asks` (r:1 w:1) + /// Proof: `Orderbook::Asks` (`max_values`: None, `max_size`: Some(80034), added: 82509, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::PendingCancellations` (r:1 w:1) + /// Proof: `Orderbook::PendingCancellations` (`max_values`: Some(1), `max_size`: Some(401), added: 896, mode: `MaxEncodedLen`) + /// The range of component `b` is `[1, 50]`. + /// The range of component `a` is `[1, 50]`. + fn on_finalize_no_matches(b: u32, a: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `709 + a * (134 ยฑ0) + b * (134 ยฑ0)` + // Estimated: `83499 + a * (2595 ยฑ0) + b * (2595 ยฑ0)` + // Minimum execution time: 338_000_000 picoseconds. + Weight::from_parts(340_000_000, 83499) + // Standard Error: 106_234 + .saturating_add(Weight::from_parts(2_374_357, 0).saturating_mul(b.into())) + // Standard Error: 106_234 + .saturating_add(Weight::from_parts(2_260_042, 0).saturating_mul(a.into())) + .saturating_add(T::DbWeight::get().reads(8_u64)) + .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(b.into()))) + .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(a.into()))) + .saturating_add(T::DbWeight::get().writes(5_u64)) + .saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(b.into()))) + .saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(a.into()))) + .saturating_add(Weight::from_parts(0, 2595).saturating_mul(a.into())) + .saturating_add(Weight::from_parts(0, 2595).saturating_mul(b.into())) + } + /// Storage: `Orderbook::Orders` (r:51 w:50) + /// Proof: `Orderbook::Orders` (`max_values`: None, `max_size`: Some(120), added: 2595, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::PendingBids` (r:2 w:1) + /// Proof: `Orderbook::PendingBids` (`max_values`: None, `max_size`: Some(8034), added: 10509, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::PendingAsks` (r:1 w:0) + /// Proof: `Orderbook::PendingAsks` (`max_values`: None, `max_size`: Some(8034), added: 10509, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::Bids` (r:1 w:1) + /// Proof: `Orderbook::Bids` (`max_values`: None, `max_size`: Some(80034), added: 82509, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::Asks` (r:1 w:0) + /// Proof: `Orderbook::Asks` (`max_values`: None, `max_size`: Some(80034), added: 82509, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::PendingCancellations` (r:1 w:1) + /// Proof: `Orderbook::PendingCancellations` (`max_values`: Some(1), `max_size`: Some(401), added: 896, mode: `MaxEncodedLen`) + /// Storage: `Assets::LockedBalance` (r:50 w:50) + /// Proof: `Assets::LockedBalance` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Assets::FreeBalance` (r:50 w:50) + /// Proof: `Assets::FreeBalance` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// The range of component `c` is `[1, 50]`. + fn on_finalize_with_cancellations(c: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `225 + c * (359 ยฑ0)` + // Estimated: `83499 + c * (2595 ยฑ0)` + // Minimum execution time: 57_000_000 picoseconds. + Weight::from_parts(45_799_510, 83499) + // Standard Error: 38_272 + .saturating_add(Weight::from_parts(20_011_078, 0).saturating_mul(c.into())) + .saturating_add(T::DbWeight::get().reads(7_u64)) + .saturating_add(T::DbWeight::get().reads((3_u64).saturating_mul(c.into()))) + .saturating_add(T::DbWeight::get().writes(3_u64)) + .saturating_add(T::DbWeight::get().writes((3_u64).saturating_mul(c.into()))) + .saturating_add(Weight::from_parts(0, 2595).saturating_mul(c.into())) + } + /// Storage: `Orderbook::Orders` (r:41 w:40) + /// Proof: `Orderbook::Orders` (`max_values`: None, `max_size`: Some(120), added: 2595, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::PendingBids` (r:2 w:1) + /// Proof: `Orderbook::PendingBids` (`max_values`: None, `max_size`: Some(8034), added: 10509, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::PendingAsks` (r:1 w:0) + /// Proof: `Orderbook::PendingAsks` (`max_values`: None, `max_size`: Some(8034), added: 10509, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::Bids` (r:1 w:0) + /// Proof: `Orderbook::Bids` (`max_values`: None, `max_size`: Some(80034), added: 82509, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::Asks` (r:2 w:0) + /// Proof: `Orderbook::Asks` (`max_values`: None, `max_size`: Some(80034), added: 82509, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::PendingCancellations` (r:1 w:1) + /// Proof: `Orderbook::PendingCancellations` (`max_values`: Some(1), `max_size`: Some(401), added: 896, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::NextTradeId` (r:1 w:1) + /// Proof: `Orderbook::NextTradeId` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Assets::LockedBalance` (r:40 w:40) + /// Proof: `Assets::LockedBalance` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Assets::FreeBalance` (r:40 w:40) + /// Proof: `Assets::FreeBalance` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::Trades` (r:0 w:20) + /// Proof: `Orderbook::Trades` (`max_values`: None, `max_size`: Some(144), added: 2619, mode: `MaxEncodedLen`) + /// The range of component `p` is `[1, 20]`. + /// The range of component `n` is `[1, 20]`. + fn on_finalize_persistent_matching(p: u32, n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0 + n * (534 ยฑ0) + p * (518 ยฑ0)` + // Estimated: `166008 + n * (2595 ยฑ58) + p * (2595 ยฑ58)` + // Minimum execution time: 203_000_000 picoseconds. + Weight::from_parts(205_000_000, 166008) + // Standard Error: 428_260 + .saturating_add(Weight::from_parts(12_659_114, 0).saturating_mul(p.into())) + // Standard Error: 428_260 + .saturating_add(Weight::from_parts(13_031_874, 0).saturating_mul(n.into())) + .saturating_add(T::DbWeight::get().reads(34_u64)) + .saturating_add(T::DbWeight::get().reads((2_u64).saturating_mul(p.into()))) + .saturating_add(T::DbWeight::get().reads((2_u64).saturating_mul(n.into()))) + .saturating_add(T::DbWeight::get().writes(30_u64)) + .saturating_add(T::DbWeight::get().writes((2_u64).saturating_mul(p.into()))) + .saturating_add(T::DbWeight::get().writes((2_u64).saturating_mul(n.into()))) + .saturating_add(Weight::from_parts(0, 2595).saturating_mul(n.into())) + .saturating_add(Weight::from_parts(0, 2595).saturating_mul(p.into())) + } + /// Storage: `Orderbook::Orders` (r:71 w:70) + /// Proof: `Orderbook::Orders` (`max_values`: None, `max_size`: Some(120), added: 2595, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::PendingBids` (r:3 w:2) + /// Proof: `Orderbook::PendingBids` (`max_values`: None, `max_size`: Some(8034), added: 10509, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::PendingAsks` (r:2 w:1) + /// Proof: `Orderbook::PendingAsks` (`max_values`: None, `max_size`: Some(8034), added: 10509, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::Bids` (r:1 w:2) + /// Proof: `Orderbook::Bids` (`max_values`: None, `max_size`: Some(80034), added: 82509, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::Asks` (r:1 w:0) + /// Proof: `Orderbook::Asks` (`max_values`: None, `max_size`: Some(80034), added: 82509, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::PendingCancellations` (r:1 w:1) + /// Proof: `Orderbook::PendingCancellations` (`max_values`: Some(1), `max_size`: Some(401), added: 896, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::NextTradeId` (r:1 w:1) + /// Proof: `Orderbook::NextTradeId` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Assets::LockedBalance` (r:50 w:50) + /// Proof: `Assets::LockedBalance` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Assets::FreeBalance` (r:50 w:50) + /// Proof: `Assets::FreeBalance` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::Trades` (r:0 w:20) + /// Proof: `Orderbook::Trades` (`max_values`: None, `max_size`: Some(144), added: 2619, mode: `MaxEncodedLen`) + /// The range of component `m` is `[1, 20]`. + /// The range of component `n` is `[1, 20]`. + /// The range of component `c` is `[1, 10]`. + fn on_finalize_complex(m: u32, n: u32, c: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `187 + c * (366 ยฑ0) + m * (713 ยฑ0) + n * (198 ยฑ0)` + // Estimated: `83499 + c * (2595 ยฑ0) + m * (5190 ยฑ0) + n * (2595 ยฑ0)` + // Minimum execution time: 429_000_000 picoseconds. + Weight::from_parts(64_754_480, 83499) + // Standard Error: 102_973 + .saturating_add(Weight::from_parts(44_112_949, 0).saturating_mul(m.into())) + // Standard Error: 102_973 + .saturating_add(Weight::from_parts(6_550_077, 0).saturating_mul(n.into())) + // Standard Error: 210_571 + .saturating_add(Weight::from_parts(21_044_681, 0).saturating_mul(c.into())) + .saturating_add(T::DbWeight::get().reads(10_u64)) + .saturating_add(T::DbWeight::get().reads((6_u64).saturating_mul(m.into()))) + .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(n.into()))) + .saturating_add(T::DbWeight::get().reads((3_u64).saturating_mul(c.into()))) + .saturating_add(T::DbWeight::get().writes(7_u64)) + .saturating_add(T::DbWeight::get().writes((7_u64).saturating_mul(m.into()))) + .saturating_add(T::DbWeight::get().writes((1_u64).saturating_mul(n.into()))) + .saturating_add(T::DbWeight::get().writes((3_u64).saturating_mul(c.into()))) + .saturating_add(Weight::from_parts(0, 2595).saturating_mul(c.into())) + .saturating_add(Weight::from_parts(0, 5190).saturating_mul(m.into())) + .saturating_add(Weight::from_parts(0, 2595).saturating_mul(n.into())) + } +} + +// For backwards compatibility and tests. +impl WeightInfo for () { + /// Storage: `Assets::FreeBalance` (r:1 w:1) + /// Proof: `Assets::FreeBalance` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Assets::LockedBalance` (r:1 w:1) + /// Proof: `Assets::LockedBalance` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::NextOrderId` (r:1 w:1) + /// Proof: `Orderbook::NextOrderId` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::PendingBids` (r:1 w:1) + /// Proof: `Orderbook::PendingBids` (`max_values`: None, `max_size`: Some(8034), added: 10509, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::UserOrders` (r:1 w:1) + /// Proof: `Orderbook::UserOrders` (`max_values`: None, `max_size`: Some(8050), added: 10525, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::Orders` (r:0 w:1) + /// Proof: `Orderbook::Orders` (`max_values`: None, `max_size`: Some(120), added: 2595, mode: `MaxEncodedLen`) + fn place_order() -> Weight { + // Proof Size summary in bytes: + // Measured: `159` + // Estimated: `11515` + // Minimum execution time: 27_000_000 picoseconds. + Weight::from_parts(28_000_000, 11515) + .saturating_add(RocksDbWeight::get().reads(5_u64)) + .saturating_add(RocksDbWeight::get().writes(6_u64)) + } + /// Storage: `Orderbook::Orders` (r:1 w:0) + /// Proof: `Orderbook::Orders` (`max_values`: None, `max_size`: Some(120), added: 2595, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::PendingCancellations` (r:1 w:1) + /// Proof: `Orderbook::PendingCancellations` (`max_values`: Some(1), `max_size`: Some(401), added: 896, mode: `MaxEncodedLen`) + fn cancel_order() -> Weight { + // Proof Size summary in bytes: + // Measured: `289` + // Estimated: `3585` + // Minimum execution time: 15_000_000 picoseconds. + Weight::from_parts(15_000_000, 3585) + .saturating_add(RocksDbWeight::get().reads(2_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `Orderbook::Orders` (r:1 w:0) + /// Proof: `Orderbook::Orders` (`max_values`: None, `max_size`: Some(120), added: 2595, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::PendingBids` (r:1 w:0) + /// Proof: `Orderbook::PendingBids` (`max_values`: None, `max_size`: Some(8034), added: 10509, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::PendingAsks` (r:1 w:0) + /// Proof: `Orderbook::PendingAsks` (`max_values`: None, `max_size`: Some(8034), added: 10509, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::Bids` (r:1 w:0) + /// Proof: `Orderbook::Bids` (`max_values`: None, `max_size`: Some(80034), added: 82509, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::Asks` (r:1 w:0) + /// Proof: `Orderbook::Asks` (`max_values`: None, `max_size`: Some(80034), added: 82509, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::PendingCancellations` (r:1 w:1) + /// Proof: `Orderbook::PendingCancellations` (`max_values`: Some(1), `max_size`: Some(401), added: 896, mode: `MaxEncodedLen`) + fn on_finalize_empty() -> Weight { + // Proof Size summary in bytes: + // Measured: `6` + // Estimated: `83499` + // Minimum execution time: 23_000_000 picoseconds. + Weight::from_parts(24_000_000, 83499) + .saturating_add(RocksDbWeight::get().reads(6_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: `Orderbook::Orders` (r:101 w:100) + /// Proof: `Orderbook::Orders` (`max_values`: None, `max_size`: Some(120), added: 2595, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::PendingBids` (r:2 w:1) + /// Proof: `Orderbook::PendingBids` (`max_values`: None, `max_size`: Some(8034), added: 10509, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::PendingAsks` (r:2 w:1) + /// Proof: `Orderbook::PendingAsks` (`max_values`: None, `max_size`: Some(8034), added: 10509, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::Bids` (r:1 w:0) + /// Proof: `Orderbook::Bids` (`max_values`: None, `max_size`: Some(80034), added: 82509, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::Asks` (r:1 w:0) + /// Proof: `Orderbook::Asks` (`max_values`: None, `max_size`: Some(80034), added: 82509, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::PendingCancellations` (r:1 w:1) + /// Proof: `Orderbook::PendingCancellations` (`max_values`: Some(1), `max_size`: Some(401), added: 896, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::NextTradeId` (r:1 w:1) + /// Proof: `Orderbook::NextTradeId` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Assets::LockedBalance` (r:100 w:100) + /// Proof: `Assets::LockedBalance` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Assets::FreeBalance` (r:100 w:100) + /// Proof: `Assets::FreeBalance` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::Trades` (r:0 w:50) + /// Proof: `Orderbook::Trades` (`max_values`: None, `max_size`: Some(144), added: 2619, mode: `MaxEncodedLen`) + /// The range of component `b` is `[1, 50]`. + /// The range of component `a` is `[1, 50]`. + fn on_finalize_with_matches(b: u32, a: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0 + a * (509 ยฑ0) + b * (510 ยฑ0)` + // Estimated: `83499 + a * (2595 ยฑ0) + b * (2595 ยฑ0)` + // Minimum execution time: 378_000_000 picoseconds. + Weight::from_parts(380_000_000, 83499) + // Standard Error: 455_618 + .saturating_add(Weight::from_parts(14_754_607, 0).saturating_mul(b.into())) + // Standard Error: 455_618 + .saturating_add(Weight::from_parts(13_465_412, 0).saturating_mul(a.into())) + .saturating_add(RocksDbWeight::get().reads(64_u64)) + .saturating_add(RocksDbWeight::get().reads((2_u64).saturating_mul(b.into()))) + .saturating_add(RocksDbWeight::get().reads((2_u64).saturating_mul(a.into()))) + .saturating_add(RocksDbWeight::get().writes(61_u64)) + .saturating_add(RocksDbWeight::get().writes((2_u64).saturating_mul(b.into()))) + .saturating_add(RocksDbWeight::get().writes((2_u64).saturating_mul(a.into()))) + .saturating_add(Weight::from_parts(0, 2595).saturating_mul(a.into())) + .saturating_add(Weight::from_parts(0, 2595).saturating_mul(b.into())) + } + /// Storage: `Orderbook::Orders` (r:101 w:100) + /// Proof: `Orderbook::Orders` (`max_values`: None, `max_size`: Some(120), added: 2595, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::PendingBids` (r:2 w:1) + /// Proof: `Orderbook::PendingBids` (`max_values`: None, `max_size`: Some(8034), added: 10509, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::PendingAsks` (r:2 w:1) + /// Proof: `Orderbook::PendingAsks` (`max_values`: None, `max_size`: Some(8034), added: 10509, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::Bids` (r:1 w:1) + /// Proof: `Orderbook::Bids` (`max_values`: None, `max_size`: Some(80034), added: 82509, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::Asks` (r:1 w:1) + /// Proof: `Orderbook::Asks` (`max_values`: None, `max_size`: Some(80034), added: 82509, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::PendingCancellations` (r:1 w:1) + /// Proof: `Orderbook::PendingCancellations` (`max_values`: Some(1), `max_size`: Some(401), added: 896, mode: `MaxEncodedLen`) + /// The range of component `b` is `[1, 50]`. + /// The range of component `a` is `[1, 50]`. + fn on_finalize_no_matches(b: u32, a: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `709 + a * (134 ยฑ0) + b * (134 ยฑ0)` + // Estimated: `83499 + a * (2595 ยฑ0) + b * (2595 ยฑ0)` + // Minimum execution time: 338_000_000 picoseconds. + Weight::from_parts(340_000_000, 83499) + // Standard Error: 106_234 + .saturating_add(Weight::from_parts(2_374_357, 0).saturating_mul(b.into())) + // Standard Error: 106_234 + .saturating_add(Weight::from_parts(2_260_042, 0).saturating_mul(a.into())) + .saturating_add(RocksDbWeight::get().reads(8_u64)) + .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(b.into()))) + .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(a.into()))) + .saturating_add(RocksDbWeight::get().writes(5_u64)) + .saturating_add(RocksDbWeight::get().writes((1_u64).saturating_mul(b.into()))) + .saturating_add(RocksDbWeight::get().writes((1_u64).saturating_mul(a.into()))) + .saturating_add(Weight::from_parts(0, 2595).saturating_mul(a.into())) + .saturating_add(Weight::from_parts(0, 2595).saturating_mul(b.into())) + } + /// Storage: `Orderbook::Orders` (r:51 w:50) + /// Proof: `Orderbook::Orders` (`max_values`: None, `max_size`: Some(120), added: 2595, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::PendingBids` (r:2 w:1) + /// Proof: `Orderbook::PendingBids` (`max_values`: None, `max_size`: Some(8034), added: 10509, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::PendingAsks` (r:1 w:0) + /// Proof: `Orderbook::PendingAsks` (`max_values`: None, `max_size`: Some(8034), added: 10509, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::Bids` (r:1 w:1) + /// Proof: `Orderbook::Bids` (`max_values`: None, `max_size`: Some(80034), added: 82509, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::Asks` (r:1 w:0) + /// Proof: `Orderbook::Asks` (`max_values`: None, `max_size`: Some(80034), added: 82509, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::PendingCancellations` (r:1 w:1) + /// Proof: `Orderbook::PendingCancellations` (`max_values`: Some(1), `max_size`: Some(401), added: 896, mode: `MaxEncodedLen`) + /// Storage: `Assets::LockedBalance` (r:50 w:50) + /// Proof: `Assets::LockedBalance` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Assets::FreeBalance` (r:50 w:50) + /// Proof: `Assets::FreeBalance` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// The range of component `c` is `[1, 50]`. + fn on_finalize_with_cancellations(c: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `225 + c * (359 ยฑ0)` + // Estimated: `83499 + c * (2595 ยฑ0)` + // Minimum execution time: 57_000_000 picoseconds. + Weight::from_parts(45_799_510, 83499) + // Standard Error: 38_272 + .saturating_add(Weight::from_parts(20_011_078, 0).saturating_mul(c.into())) + .saturating_add(RocksDbWeight::get().reads(7_u64)) + .saturating_add(RocksDbWeight::get().reads((3_u64).saturating_mul(c.into()))) + .saturating_add(RocksDbWeight::get().writes(3_u64)) + .saturating_add(RocksDbWeight::get().writes((3_u64).saturating_mul(c.into()))) + .saturating_add(Weight::from_parts(0, 2595).saturating_mul(c.into())) + } + /// Storage: `Orderbook::Orders` (r:41 w:40) + /// Proof: `Orderbook::Orders` (`max_values`: None, `max_size`: Some(120), added: 2595, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::PendingBids` (r:2 w:1) + /// Proof: `Orderbook::PendingBids` (`max_values`: None, `max_size`: Some(8034), added: 10509, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::PendingAsks` (r:1 w:0) + /// Proof: `Orderbook::PendingAsks` (`max_values`: None, `max_size`: Some(8034), added: 10509, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::Bids` (r:1 w:0) + /// Proof: `Orderbook::Bids` (`max_values`: None, `max_size`: Some(80034), added: 82509, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::Asks` (r:2 w:0) + /// Proof: `Orderbook::Asks` (`max_values`: None, `max_size`: Some(80034), added: 82509, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::PendingCancellations` (r:1 w:1) + /// Proof: `Orderbook::PendingCancellations` (`max_values`: Some(1), `max_size`: Some(401), added: 896, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::NextTradeId` (r:1 w:1) + /// Proof: `Orderbook::NextTradeId` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Assets::LockedBalance` (r:40 w:40) + /// Proof: `Assets::LockedBalance` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Assets::FreeBalance` (r:40 w:40) + /// Proof: `Assets::FreeBalance` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::Trades` (r:0 w:20) + /// Proof: `Orderbook::Trades` (`max_values`: None, `max_size`: Some(144), added: 2619, mode: `MaxEncodedLen`) + /// The range of component `p` is `[1, 20]`. + /// The range of component `n` is `[1, 20]`. + fn on_finalize_persistent_matching(p: u32, n: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0 + n * (534 ยฑ0) + p * (518 ยฑ0)` + // Estimated: `166008 + n * (2595 ยฑ58) + p * (2595 ยฑ58)` + // Minimum execution time: 203_000_000 picoseconds. + Weight::from_parts(205_000_000, 166008) + // Standard Error: 428_260 + .saturating_add(Weight::from_parts(12_659_114, 0).saturating_mul(p.into())) + // Standard Error: 428_260 + .saturating_add(Weight::from_parts(13_031_874, 0).saturating_mul(n.into())) + .saturating_add(RocksDbWeight::get().reads(34_u64)) + .saturating_add(RocksDbWeight::get().reads((2_u64).saturating_mul(p.into()))) + .saturating_add(RocksDbWeight::get().reads((2_u64).saturating_mul(n.into()))) + .saturating_add(RocksDbWeight::get().writes(30_u64)) + .saturating_add(RocksDbWeight::get().writes((2_u64).saturating_mul(p.into()))) + .saturating_add(RocksDbWeight::get().writes((2_u64).saturating_mul(n.into()))) + .saturating_add(Weight::from_parts(0, 2595).saturating_mul(n.into())) + .saturating_add(Weight::from_parts(0, 2595).saturating_mul(p.into())) + } + /// Storage: `Orderbook::Orders` (r:71 w:70) + /// Proof: `Orderbook::Orders` (`max_values`: None, `max_size`: Some(120), added: 2595, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::PendingBids` (r:3 w:2) + /// Proof: `Orderbook::PendingBids` (`max_values`: None, `max_size`: Some(8034), added: 10509, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::PendingAsks` (r:2 w:1) + /// Proof: `Orderbook::PendingAsks` (`max_values`: None, `max_size`: Some(8034), added: 10509, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::Bids` (r:1 w:2) + /// Proof: `Orderbook::Bids` (`max_values`: None, `max_size`: Some(80034), added: 82509, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::Asks` (r:1 w:0) + /// Proof: `Orderbook::Asks` (`max_values`: None, `max_size`: Some(80034), added: 82509, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::PendingCancellations` (r:1 w:1) + /// Proof: `Orderbook::PendingCancellations` (`max_values`: Some(1), `max_size`: Some(401), added: 896, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::NextTradeId` (r:1 w:1) + /// Proof: `Orderbook::NextTradeId` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Assets::LockedBalance` (r:50 w:50) + /// Proof: `Assets::LockedBalance` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Assets::FreeBalance` (r:50 w:50) + /// Proof: `Assets::FreeBalance` (`max_values`: None, `max_size`: Some(84), added: 2559, mode: `MaxEncodedLen`) + /// Storage: `Orderbook::Trades` (r:0 w:20) + /// Proof: `Orderbook::Trades` (`max_values`: None, `max_size`: Some(144), added: 2619, mode: `MaxEncodedLen`) + /// The range of component `m` is `[1, 20]`. + /// The range of component `n` is `[1, 20]`. + /// The range of component `c` is `[1, 10]`. + fn on_finalize_complex(m: u32, n: u32, c: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `187 + c * (366 ยฑ0) + m * (713 ยฑ0) + n * (198 ยฑ0)` + // Estimated: `83499 + c * (2595 ยฑ0) + m * (5190 ยฑ0) + n * (2595 ยฑ0)` + // Minimum execution time: 429_000_000 picoseconds. + Weight::from_parts(64_754_480, 83499) + // Standard Error: 102_973 + .saturating_add(Weight::from_parts(44_112_949, 0).saturating_mul(m.into())) + // Standard Error: 102_973 + .saturating_add(Weight::from_parts(6_550_077, 0).saturating_mul(n.into())) + // Standard Error: 210_571 + .saturating_add(Weight::from_parts(21_044_681, 0).saturating_mul(c.into())) + .saturating_add(RocksDbWeight::get().reads(10_u64)) + .saturating_add(RocksDbWeight::get().reads((6_u64).saturating_mul(m.into()))) + .saturating_add(RocksDbWeight::get().reads((1_u64).saturating_mul(n.into()))) + .saturating_add(RocksDbWeight::get().reads((3_u64).saturating_mul(c.into()))) + .saturating_add(RocksDbWeight::get().writes(7_u64)) + .saturating_add(RocksDbWeight::get().writes((7_u64).saturating_mul(m.into()))) + .saturating_add(RocksDbWeight::get().writes((1_u64).saturating_mul(n.into()))) + .saturating_add(RocksDbWeight::get().writes((3_u64).saturating_mul(c.into()))) + .saturating_add(Weight::from_parts(0, 2595).saturating_mul(c.into())) + .saturating_add(Weight::from_parts(0, 5190).saturating_mul(m.into())) + .saturating_add(Weight::from_parts(0, 2595).saturating_mul(n.into())) + } +} diff --git a/pallets/template/src/benchmarking.rs b/pallets/template/src/benchmarking.rs index 8af5d24..6dd7cfa 100644 --- a/pallets/template/src/benchmarking.rs +++ b/pallets/template/src/benchmarking.rs @@ -9,27 +9,27 @@ use frame_system::RawOrigin; #[benchmarks] mod benchmarks { - use super::*; + use super::*; - #[benchmark] - fn do_something() { - let value = 100u32; - let caller: T::AccountId = whitelisted_caller(); - #[extrinsic_call] - do_something(RawOrigin::Signed(caller), value); + #[benchmark] + fn do_something() { + let value = 100u32; + let caller: T::AccountId = whitelisted_caller(); + #[extrinsic_call] + do_something(RawOrigin::Signed(caller), value); - assert_eq!(Something::::get(), Some(value)); - } + assert_eq!(Something::::get(), Some(value)); + } - #[benchmark] - fn cause_error() { - Something::::put(100u32); - let caller: T::AccountId = whitelisted_caller(); - #[extrinsic_call] - cause_error(RawOrigin::Signed(caller)); + #[benchmark] + fn cause_error() { + Something::::put(100u32); + let caller: T::AccountId = whitelisted_caller(); + #[extrinsic_call] + cause_error(RawOrigin::Signed(caller)); - assert_eq!(Something::::get(), Some(101u32)); - } + assert_eq!(Something::::get(), Some(101u32)); + } - impl_benchmark_test_suite!(Template, crate::mock::new_test_ext(), crate::mock::Test); + impl_benchmark_test_suite!(Template, crate::mock::new_test_ext(), crate::mock::Test); } diff --git a/pallets/template/src/lib.rs b/pallets/template/src/lib.rs index 90dfe37..4e605e9 100644 --- a/pallets/template/src/lib.rs +++ b/pallets/template/src/lib.rs @@ -63,140 +63,140 @@ pub use weights::*; // All pallet logic is defined in its own module and must be annotated by the `pallet` attribute. #[frame_support::pallet] pub mod pallet { - // Import various useful types required by all FRAME pallets. - use super::*; - use frame_support::pallet_prelude::*; - use frame_system::pallet_prelude::*; - - // The `Pallet` struct serves as a placeholder to implement traits, methods and dispatchables - // (`Call`s) in this pallet. - #[pallet::pallet] - pub struct Pallet(_); - - /// The pallet's configuration trait. - /// - /// All our types and constants a pallet depends on must be declared here. - /// These types are defined generically and made concrete when the pallet is declared in the - /// `runtime/src/lib.rs` file of your chain. - #[pallet::config] - pub trait Config: frame_system::Config { - /// The overarching runtime event type. - type RuntimeEvent: From> + IsType<::RuntimeEvent>; - /// A type representing the weights required by the dispatchables of this pallet. - type WeightInfo: WeightInfo; - } - - /// A storage item for this pallet. - /// - /// In this template, we are declaring a storage item called `Something` that stores a single - /// `u32` value. Learn more about runtime storage here: - #[pallet::storage] - pub type Something = StorageValue<_, u32>; - - /// Events that functions in this pallet can emit. - /// - /// Events are a simple means of indicating to the outside world (such as dApps, chain explorers - /// or other users) that some notable update in the runtime has occurred. In a FRAME pallet, the - /// documentation for each event field and its parameters is added to a node's metadata so it - /// can be used by external interfaces or tools. - /// - /// The `generate_deposit` macro generates a function on `Pallet` called `deposit_event` which - /// will convert the event type of your pallet into `RuntimeEvent` (declared in the pallet's - /// [`Config`] trait) and deposit it using [`frame_system::Pallet::deposit_event`]. - #[pallet::event] - #[pallet::generate_deposit(pub(super) fn deposit_event)] - pub enum Event { - /// A user has successfully set a new value. - SomethingStored { - /// The new value set. - something: u32, - /// The account who set the new value. - who: T::AccountId, - }, - } - - /// Errors that can be returned by this pallet. - /// - /// Errors tell users that something went wrong so it's important that their naming is - /// informative. Similar to events, error documentation is added to a node's metadata so it's - /// equally important that they have helpful documentation associated with them. - /// - /// This type of runtime error can be up to 4 bytes in size should you want to return additional - /// information. - #[pallet::error] - pub enum Error { - /// The value retrieved was `None` as no value was previously set. - NoneValue, - /// There was an attempt to increment the value in storage over `u32::MAX`. - StorageOverflow, - } - - /// The pallet's dispatchable functions ([`Call`]s). - /// - /// Dispatchable functions allows users to interact with the pallet and invoke state changes. - /// These functions materialize as "extrinsics", which are often compared to transactions. - /// They must always return a `DispatchResult` and be annotated with a weight and call index. - /// - /// The [`call_index`] macro is used to explicitly - /// define an index for calls in the [`Call`] enum. This is useful for pallets that may - /// introduce new dispatchables over time. If the order of a dispatchable changes, its index - /// will also change which will break backwards compatibility. - /// - /// The [`weight`] macro is used to assign a weight to each call. - #[pallet::call] - impl Pallet { - /// An example dispatchable that takes a single u32 value as a parameter, writes the value - /// to storage and emits an event. - /// - /// It checks that the _origin_ for this call is _Signed_ and returns a dispatch - /// error if it isn't. Learn more about origins here: - #[pallet::call_index(0)] - #[pallet::weight(T::WeightInfo::do_something())] - pub fn do_something(origin: OriginFor, something: u32) -> DispatchResult { - // Check that the extrinsic was signed and get the signer. - let who = ensure_signed(origin)?; - - // Update storage. - Something::::put(something); - - // Emit an event. - Self::deposit_event(Event::SomethingStored { something, who }); - - // Return a successful `DispatchResult` - Ok(()) - } - - /// An example dispatchable that may throw a custom error. - /// - /// It checks that the caller is a signed origin and reads the current value from the - /// `Something` storage item. If a current value exists, it is incremented by 1 and then - /// written back to storage. - /// - /// ## Errors - /// - /// The function will return an error under the following conditions: - /// - /// - If no value has been set ([`Error::NoneValue`]) - /// - If incrementing the value in storage causes an arithmetic overflow - /// ([`Error::StorageOverflow`]) - #[pallet::call_index(1)] - #[pallet::weight(T::WeightInfo::cause_error())] - pub fn cause_error(origin: OriginFor) -> DispatchResult { - let _who = ensure_signed(origin)?; - - // Read a value from storage. - match Something::::get() { - // Return an error if the value has not been set. - None => Err(Error::::NoneValue.into()), - Some(old) => { - // Increment the value read from storage. This will cause an error in the event - // of overflow. - let new = old.checked_add(1).ok_or(Error::::StorageOverflow)?; - // Update the value in storage with the incremented result. - Something::::put(new); - Ok(()) - }, - } - } - } + // Import various useful types required by all FRAME pallets. + use super::*; + use frame_support::pallet_prelude::*; + use frame_system::pallet_prelude::*; + + // The `Pallet` struct serves as a placeholder to implement traits, methods and dispatchables + // (`Call`s) in this pallet. + #[pallet::pallet] + pub struct Pallet(_); + + /// The pallet's configuration trait. + /// + /// All our types and constants a pallet depends on must be declared here. + /// These types are defined generically and made concrete when the pallet is declared in the + /// `runtime/src/lib.rs` file of your chain. + #[pallet::config] + pub trait Config: frame_system::Config { + /// The overarching runtime event type. + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + /// A type representing the weights required by the dispatchables of this pallet. + type WeightInfo: WeightInfo; + } + + /// A storage item for this pallet. + /// + /// In this template, we are declaring a storage item called `Something` that stores a single + /// `u32` value. Learn more about runtime storage here: + #[pallet::storage] + pub type Something = StorageValue<_, u32>; + + /// Events that functions in this pallet can emit. + /// + /// Events are a simple means of indicating to the outside world (such as dApps, chain explorers + /// or other users) that some notable update in the runtime has occurred. In a FRAME pallet, the + /// documentation for each event field and its parameters is added to a node's metadata so it + /// can be used by external interfaces or tools. + /// + /// The `generate_deposit` macro generates a function on `Pallet` called `deposit_event` which + /// will convert the event type of your pallet into `RuntimeEvent` (declared in the pallet's + /// [`Config`] trait) and deposit it using [`frame_system::Pallet::deposit_event`]. + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// A user has successfully set a new value. + SomethingStored { + /// The new value set. + something: u32, + /// The account who set the new value. + who: T::AccountId, + }, + } + + /// Errors that can be returned by this pallet. + /// + /// Errors tell users that something went wrong so it's important that their naming is + /// informative. Similar to events, error documentation is added to a node's metadata so it's + /// equally important that they have helpful documentation associated with them. + /// + /// This type of runtime error can be up to 4 bytes in size should you want to return additional + /// information. + #[pallet::error] + pub enum Error { + /// The value retrieved was `None` as no value was previously set. + NoneValue, + /// There was an attempt to increment the value in storage over `u32::MAX`. + StorageOverflow, + } + + /// The pallet's dispatchable functions ([`Call`]s). + /// + /// Dispatchable functions allows users to interact with the pallet and invoke state changes. + /// These functions materialize as "extrinsics", which are often compared to transactions. + /// They must always return a `DispatchResult` and be annotated with a weight and call index. + /// + /// The [`call_index`] macro is used to explicitly + /// define an index for calls in the [`Call`] enum. This is useful for pallets that may + /// introduce new dispatchables over time. If the order of a dispatchable changes, its index + /// will also change which will break backwards compatibility. + /// + /// The [`weight`] macro is used to assign a weight to each call. + #[pallet::call] + impl Pallet { + /// An example dispatchable that takes a single u32 value as a parameter, writes the value + /// to storage and emits an event. + /// + /// It checks that the _origin_ for this call is _Signed_ and returns a dispatch + /// error if it isn't. Learn more about origins here: + #[pallet::call_index(0)] + #[pallet::weight(T::WeightInfo::do_something())] + pub fn do_something(origin: OriginFor, something: u32) -> DispatchResult { + // Check that the extrinsic was signed and get the signer. + let who = ensure_signed(origin)?; + + // Update storage. + Something::::put(something); + + // Emit an event. + Self::deposit_event(Event::SomethingStored { something, who }); + + // Return a successful `DispatchResult` + Ok(()) + } + + /// An example dispatchable that may throw a custom error. + /// + /// It checks that the caller is a signed origin and reads the current value from the + /// `Something` storage item. If a current value exists, it is incremented by 1 and then + /// written back to storage. + /// + /// ## Errors + /// + /// The function will return an error under the following conditions: + /// + /// - If no value has been set ([`Error::NoneValue`]) + /// - If incrementing the value in storage causes an arithmetic overflow + /// ([`Error::StorageOverflow`]) + #[pallet::call_index(1)] + #[pallet::weight(T::WeightInfo::cause_error())] + pub fn cause_error(origin: OriginFor) -> DispatchResult { + let _who = ensure_signed(origin)?; + + // Read a value from storage. + match Something::::get() { + // Return an error if the value has not been set. + None => Err(Error::::NoneValue.into()), + Some(old) => { + // Increment the value read from storage. This will cause an error in the event + // of overflow. + let new = old.checked_add(1).ok_or(Error::::StorageOverflow)?; + // Update the value in storage with the incremented result. + Something::::put(new); + Ok(()) + } + } + } + } } diff --git a/pallets/template/src/mock.rs b/pallets/template/src/mock.rs index 44085bc..d91b165 100644 --- a/pallets/template/src/mock.rs +++ b/pallets/template/src/mock.rs @@ -6,41 +6,44 @@ type Block = frame_system::mocking::MockBlock; #[frame_support::runtime] mod runtime { - // The main runtime - #[runtime::runtime] - // Runtime Types to be generated - #[runtime::derive( - RuntimeCall, - RuntimeEvent, - RuntimeError, - RuntimeOrigin, - RuntimeFreezeReason, - RuntimeHoldReason, - RuntimeSlashReason, - RuntimeLockId, - RuntimeTask, - RuntimeViewFunction - )] - pub struct Test; + // The main runtime + #[runtime::runtime] + // Runtime Types to be generated + #[runtime::derive( + RuntimeCall, + RuntimeEvent, + RuntimeError, + RuntimeOrigin, + RuntimeFreezeReason, + RuntimeHoldReason, + RuntimeSlashReason, + RuntimeLockId, + RuntimeTask, + RuntimeViewFunction + )] + pub struct Test; - #[runtime::pallet_index(0)] - pub type System = frame_system::Pallet; + #[runtime::pallet_index(0)] + pub type System = frame_system::Pallet; - #[runtime::pallet_index(1)] - pub type Template = pallet_template::Pallet; + #[runtime::pallet_index(1)] + pub type Template = pallet_template::Pallet; } #[derive_impl(frame_system::config_preludes::TestDefaultConfig)] impl frame_system::Config for Test { - type Block = Block; + type Block = Block; } impl pallet_template::Config for Test { - type RuntimeEvent = RuntimeEvent; - type WeightInfo = (); + type RuntimeEvent = RuntimeEvent; + type WeightInfo = (); } // Build genesis storage according to the mock runtime. pub fn new_test_ext() -> sp_io::TestExternalities { - frame_system::GenesisConfig::::default().build_storage().unwrap().into() + frame_system::GenesisConfig::::default() + .build_storage() + .unwrap() + .into() } diff --git a/pallets/template/src/tests.rs b/pallets/template/src/tests.rs index d05433c..3dcb7d5 100644 --- a/pallets/template/src/tests.rs +++ b/pallets/template/src/tests.rs @@ -3,22 +3,31 @@ use frame_support::{assert_noop, assert_ok}; #[test] fn it_works_for_default_value() { - new_test_ext().execute_with(|| { - // Go past genesis block so events get deposited - System::set_block_number(1); - // Dispatch a signed extrinsic. - assert_ok!(Template::do_something(RuntimeOrigin::signed(1), 42)); - // Read pallet storage and assert an expected result. - assert_eq!(Something::::get(), Some(42)); - // Assert that the correct event was deposited - System::assert_last_event(Event::SomethingStored { something: 42, who: 1 }.into()); - }); + new_test_ext().execute_with(|| { + // Go past genesis block so events get deposited + System::set_block_number(1); + // Dispatch a signed extrinsic. + assert_ok!(Template::do_something(RuntimeOrigin::signed(1), 42)); + // Read pallet storage and assert an expected result. + assert_eq!(Something::::get(), Some(42)); + // Assert that the correct event was deposited + System::assert_last_event( + Event::SomethingStored { + something: 42, + who: 1, + } + .into(), + ); + }); } #[test] fn correct_error_for_none_value() { - new_test_ext().execute_with(|| { - // Ensure the expected error is thrown when no value is present. - assert_noop!(Template::cause_error(RuntimeOrigin::signed(1)), Error::::NoneValue); - }); + new_test_ext().execute_with(|| { + // Ensure the expected error is thrown when no value is present. + assert_noop!( + Template::cause_error(RuntimeOrigin::signed(1)), + Error::::NoneValue + ); + }); } diff --git a/runtime/build.rs b/runtime/build.rs index caac851..138ae65 100644 --- a/runtime/build.rs +++ b/runtime/build.rs @@ -1,13 +1,13 @@ #[cfg(all(feature = "std", feature = "metadata-hash"))] fn main() { - substrate_wasm_builder::WasmBuilder::init_with_defaults() - .enable_metadata_hash("UNIT", 12) - .build(); + substrate_wasm_builder::WasmBuilder::init_with_defaults() + .enable_metadata_hash("UNIT", 12) + .build(); } #[cfg(all(feature = "std", not(feature = "metadata-hash")))] fn main() { - substrate_wasm_builder::WasmBuilder::build_using_defaults(); + substrate_wasm_builder::WasmBuilder::build_using_defaults(); } /// The wasm builder is deactivated when compiling diff --git a/runtime/src/apis.rs b/runtime/src/apis.rs index 0288070..d336d2e 100644 --- a/runtime/src/apis.rs +++ b/runtime/src/apis.rs @@ -26,279 +26,279 @@ // External crates imports use alloc::vec::Vec; use frame_support::{ - genesis_builder_helper::{build_state, get_preset}, - weights::Weight, + genesis_builder_helper::{build_state, get_preset}, + weights::Weight, }; use pallet_grandpa::AuthorityId as GrandpaId; use sp_api::impl_runtime_apis; use sp_consensus_aura::sr25519::AuthorityId as AuraId; use sp_core::{crypto::KeyTypeId, OpaqueMetadata}; use sp_runtime::{ - traits::{Block as BlockT, NumberFor}, - transaction_validity::{TransactionSource, TransactionValidity}, - ApplyExtrinsicResult, + traits::{Block as BlockT, NumberFor}, + transaction_validity::{TransactionSource, TransactionValidity}, + ApplyExtrinsicResult, }; use sp_version::RuntimeVersion; // Local module imports use super::{ - AccountId, Aura, Balance, Block, Executive, Grandpa, InherentDataExt, Nonce, Runtime, - RuntimeCall, RuntimeGenesisConfig, SessionKeys, System, TransactionPayment, VERSION, + AccountId, Aura, Balance, Block, Executive, Grandpa, InherentDataExt, Nonce, Runtime, + RuntimeCall, RuntimeGenesisConfig, SessionKeys, System, TransactionPayment, VERSION, }; impl_runtime_apis! { - impl sp_api::Core for Runtime { - fn version() -> RuntimeVersion { - VERSION - } - - fn execute_block(block: Block) { - Executive::execute_block(block); - } - - fn initialize_block(header: &::Header) -> sp_runtime::ExtrinsicInclusionMode { - Executive::initialize_block(header) - } - } - - impl sp_api::Metadata for Runtime { - fn metadata() -> OpaqueMetadata { - OpaqueMetadata::new(Runtime::metadata().into()) - } - - fn metadata_at_version(version: u32) -> Option { - Runtime::metadata_at_version(version) - } - - fn metadata_versions() -> Vec { - Runtime::metadata_versions() - } - } - - impl frame_support::view_functions::runtime_api::RuntimeViewFunction for Runtime { - fn execute_view_function(id: frame_support::view_functions::ViewFunctionId, input: Vec) -> Result, frame_support::view_functions::ViewFunctionDispatchError> { - Runtime::execute_view_function(id, input) - } - } - - impl sp_block_builder::BlockBuilder for Runtime { - fn apply_extrinsic(extrinsic: ::Extrinsic) -> ApplyExtrinsicResult { - Executive::apply_extrinsic(extrinsic) - } - - fn finalize_block() -> ::Header { - Executive::finalize_block() - } - - fn inherent_extrinsics(data: sp_inherents::InherentData) -> Vec<::Extrinsic> { - data.create_extrinsics() - } - - fn check_inherents( - block: Block, - data: sp_inherents::InherentData, - ) -> sp_inherents::CheckInherentsResult { - data.check_extrinsics(&block) - } - } - - impl sp_transaction_pool::runtime_api::TaggedTransactionQueue for Runtime { - fn validate_transaction( - source: TransactionSource, - tx: ::Extrinsic, - block_hash: ::Hash, - ) -> TransactionValidity { - Executive::validate_transaction(source, tx, block_hash) - } - } - - impl sp_offchain::OffchainWorkerApi for Runtime { - fn offchain_worker(header: &::Header) { - Executive::offchain_worker(header) - } - } - - impl sp_consensus_aura::AuraApi for Runtime { - fn slot_duration() -> sp_consensus_aura::SlotDuration { - sp_consensus_aura::SlotDuration::from_millis(Aura::slot_duration()) - } - - fn authorities() -> Vec { - pallet_aura::Authorities::::get().into_inner() - } - } - - impl sp_session::SessionKeys for Runtime { - fn generate_session_keys(seed: Option>) -> Vec { - SessionKeys::generate(seed) - } - - fn decode_session_keys( - encoded: Vec, - ) -> Option, KeyTypeId)>> { - SessionKeys::decode_into_raw_public_keys(&encoded) - } - } - - impl sp_consensus_grandpa::GrandpaApi for Runtime { - fn grandpa_authorities() -> sp_consensus_grandpa::AuthorityList { - Grandpa::grandpa_authorities() - } - - fn current_set_id() -> sp_consensus_grandpa::SetId { - Grandpa::current_set_id() - } - - fn submit_report_equivocation_unsigned_extrinsic( - _equivocation_proof: sp_consensus_grandpa::EquivocationProof< - ::Hash, - NumberFor, - >, - _key_owner_proof: sp_consensus_grandpa::OpaqueKeyOwnershipProof, - ) -> Option<()> { - None - } - - fn generate_key_ownership_proof( - _set_id: sp_consensus_grandpa::SetId, - _authority_id: GrandpaId, - ) -> Option { - // NOTE: this is the only implementation possible since we've - // defined our key owner proof type as a bottom type (i.e. a type - // with no values). - None - } - } - - impl frame_system_rpc_runtime_api::AccountNonceApi for Runtime { - fn account_nonce(account: AccountId) -> Nonce { - System::account_nonce(account) - } - } - - impl pallet_transaction_payment_rpc_runtime_api::TransactionPaymentApi for Runtime { - fn query_info( - uxt: ::Extrinsic, - len: u32, - ) -> pallet_transaction_payment_rpc_runtime_api::RuntimeDispatchInfo { - TransactionPayment::query_info(uxt, len) - } - fn query_fee_details( - uxt: ::Extrinsic, - len: u32, - ) -> pallet_transaction_payment::FeeDetails { - TransactionPayment::query_fee_details(uxt, len) - } - fn query_weight_to_fee(weight: Weight) -> Balance { - TransactionPayment::weight_to_fee(weight) - } - fn query_length_to_fee(length: u32) -> Balance { - TransactionPayment::length_to_fee(length) - } - } - - impl pallet_transaction_payment_rpc_runtime_api::TransactionPaymentCallApi - for Runtime - { - fn query_call_info( - call: RuntimeCall, - len: u32, - ) -> pallet_transaction_payment::RuntimeDispatchInfo { - TransactionPayment::query_call_info(call, len) - } - fn query_call_fee_details( - call: RuntimeCall, - len: u32, - ) -> pallet_transaction_payment::FeeDetails { - TransactionPayment::query_call_fee_details(call, len) - } - fn query_weight_to_fee(weight: Weight) -> Balance { - TransactionPayment::weight_to_fee(weight) - } - fn query_length_to_fee(length: u32) -> Balance { - TransactionPayment::length_to_fee(length) - } - } - - #[cfg(feature = "runtime-benchmarks")] - impl frame_benchmarking::Benchmark for Runtime { - fn benchmark_metadata(extra: bool) -> ( - Vec, - Vec, - ) { - use frame_benchmarking::{baseline, BenchmarkList}; - use frame_support::traits::StorageInfoTrait; - use frame_system_benchmarking::Pallet as SystemBench; - use frame_system_benchmarking::extensions::Pallet as SystemExtensionsBench; - use baseline::Pallet as BaselineBench; - use super::*; - - let mut list = Vec::::new(); - list_benchmarks!(list, extra); - - let storage_info = AllPalletsWithSystem::storage_info(); - - (list, storage_info) - } - - #[allow(non_local_definitions)] - fn dispatch_benchmark( - config: frame_benchmarking::BenchmarkConfig - ) -> Result, alloc::string::String> { - use frame_benchmarking::{baseline, BenchmarkBatch}; - use sp_storage::TrackedStorageKey; - use frame_system_benchmarking::Pallet as SystemBench; - use frame_system_benchmarking::extensions::Pallet as SystemExtensionsBench; - use baseline::Pallet as BaselineBench; - use super::*; - - impl frame_system_benchmarking::Config for Runtime {} - impl baseline::Config for Runtime {} - - use frame_support::traits::WhitelistedStorageKeys; - let whitelist: Vec = AllPalletsWithSystem::whitelisted_storage_keys(); - - let mut batches = Vec::::new(); - let params = (&config, &whitelist); - add_benchmarks!(params, batches); - - Ok(batches) - } - } - - #[cfg(feature = "try-runtime")] - impl frame_try_runtime::TryRuntime for Runtime { - fn on_runtime_upgrade(checks: frame_try_runtime::UpgradeCheckSelect) -> (Weight, Weight) { - // NOTE: intentional unwrap: we don't want to propagate the error backwards, and want to - // have a backtrace here. If any of the pre/post migration checks fail, we shall stop - // right here and right now. - let weight = Executive::try_runtime_upgrade(checks).unwrap(); - (weight, super::configs::RuntimeBlockWeights::get().max_block) - } - - fn execute_block( - block: Block, - state_root_check: bool, - signature_check: bool, - select: frame_try_runtime::TryStateSelect - ) -> Weight { - // NOTE: intentional unwrap: we don't want to propagate the error backwards, and want to - // have a backtrace here. - Executive::try_execute_block(block, state_root_check, signature_check, select).expect("execute-block failed") - } - } - - impl sp_genesis_builder::GenesisBuilder for Runtime { - fn build_state(config: Vec) -> sp_genesis_builder::Result { - build_state::(config) - } - - fn get_preset(id: &Option) -> Option> { - get_preset::(id, crate::genesis_config_presets::get_preset) - } - - fn preset_names() -> Vec { - crate::genesis_config_presets::preset_names() - } - } + impl sp_api::Core for Runtime { + fn version() -> RuntimeVersion { + VERSION + } + + fn execute_block(block: Block) { + Executive::execute_block(block); + } + + fn initialize_block(header: &::Header) -> sp_runtime::ExtrinsicInclusionMode { + Executive::initialize_block(header) + } + } + + impl sp_api::Metadata for Runtime { + fn metadata() -> OpaqueMetadata { + OpaqueMetadata::new(Runtime::metadata().into()) + } + + fn metadata_at_version(version: u32) -> Option { + Runtime::metadata_at_version(version) + } + + fn metadata_versions() -> Vec { + Runtime::metadata_versions() + } + } + + impl frame_support::view_functions::runtime_api::RuntimeViewFunction for Runtime { + fn execute_view_function(id: frame_support::view_functions::ViewFunctionId, input: Vec) -> Result, frame_support::view_functions::ViewFunctionDispatchError> { + Runtime::execute_view_function(id, input) + } + } + + impl sp_block_builder::BlockBuilder for Runtime { + fn apply_extrinsic(extrinsic: ::Extrinsic) -> ApplyExtrinsicResult { + Executive::apply_extrinsic(extrinsic) + } + + fn finalize_block() -> ::Header { + Executive::finalize_block() + } + + fn inherent_extrinsics(data: sp_inherents::InherentData) -> Vec<::Extrinsic> { + data.create_extrinsics() + } + + fn check_inherents( + block: Block, + data: sp_inherents::InherentData, + ) -> sp_inherents::CheckInherentsResult { + data.check_extrinsics(&block) + } + } + + impl sp_transaction_pool::runtime_api::TaggedTransactionQueue for Runtime { + fn validate_transaction( + source: TransactionSource, + tx: ::Extrinsic, + block_hash: ::Hash, + ) -> TransactionValidity { + Executive::validate_transaction(source, tx, block_hash) + } + } + + impl sp_offchain::OffchainWorkerApi for Runtime { + fn offchain_worker(header: &::Header) { + Executive::offchain_worker(header) + } + } + + impl sp_consensus_aura::AuraApi for Runtime { + fn slot_duration() -> sp_consensus_aura::SlotDuration { + sp_consensus_aura::SlotDuration::from_millis(Aura::slot_duration()) + } + + fn authorities() -> Vec { + pallet_aura::Authorities::::get().into_inner() + } + } + + impl sp_session::SessionKeys for Runtime { + fn generate_session_keys(seed: Option>) -> Vec { + SessionKeys::generate(seed) + } + + fn decode_session_keys( + encoded: Vec, + ) -> Option, KeyTypeId)>> { + SessionKeys::decode_into_raw_public_keys(&encoded) + } + } + + impl sp_consensus_grandpa::GrandpaApi for Runtime { + fn grandpa_authorities() -> sp_consensus_grandpa::AuthorityList { + Grandpa::grandpa_authorities() + } + + fn current_set_id() -> sp_consensus_grandpa::SetId { + Grandpa::current_set_id() + } + + fn submit_report_equivocation_unsigned_extrinsic( + _equivocation_proof: sp_consensus_grandpa::EquivocationProof< + ::Hash, + NumberFor, + >, + _key_owner_proof: sp_consensus_grandpa::OpaqueKeyOwnershipProof, + ) -> Option<()> { + None + } + + fn generate_key_ownership_proof( + _set_id: sp_consensus_grandpa::SetId, + _authority_id: GrandpaId, + ) -> Option { + // NOTE: this is the only implementation possible since we've + // defined our key owner proof type as a bottom type (i.e. a type + // with no values). + None + } + } + + impl frame_system_rpc_runtime_api::AccountNonceApi for Runtime { + fn account_nonce(account: AccountId) -> Nonce { + System::account_nonce(account) + } + } + + impl pallet_transaction_payment_rpc_runtime_api::TransactionPaymentApi for Runtime { + fn query_info( + uxt: ::Extrinsic, + len: u32, + ) -> pallet_transaction_payment_rpc_runtime_api::RuntimeDispatchInfo { + TransactionPayment::query_info(uxt, len) + } + fn query_fee_details( + uxt: ::Extrinsic, + len: u32, + ) -> pallet_transaction_payment::FeeDetails { + TransactionPayment::query_fee_details(uxt, len) + } + fn query_weight_to_fee(weight: Weight) -> Balance { + TransactionPayment::weight_to_fee(weight) + } + fn query_length_to_fee(length: u32) -> Balance { + TransactionPayment::length_to_fee(length) + } + } + + impl pallet_transaction_payment_rpc_runtime_api::TransactionPaymentCallApi + for Runtime + { + fn query_call_info( + call: RuntimeCall, + len: u32, + ) -> pallet_transaction_payment::RuntimeDispatchInfo { + TransactionPayment::query_call_info(call, len) + } + fn query_call_fee_details( + call: RuntimeCall, + len: u32, + ) -> pallet_transaction_payment::FeeDetails { + TransactionPayment::query_call_fee_details(call, len) + } + fn query_weight_to_fee(weight: Weight) -> Balance { + TransactionPayment::weight_to_fee(weight) + } + fn query_length_to_fee(length: u32) -> Balance { + TransactionPayment::length_to_fee(length) + } + } + + #[cfg(feature = "runtime-benchmarks")] + impl frame_benchmarking::Benchmark for Runtime { + fn benchmark_metadata(extra: bool) -> ( + Vec, + Vec, + ) { + use frame_benchmarking::{baseline, BenchmarkList}; + use frame_support::traits::StorageInfoTrait; + use frame_system_benchmarking::Pallet as SystemBench; + use frame_system_benchmarking::extensions::Pallet as SystemExtensionsBench; + use baseline::Pallet as BaselineBench; + use super::*; + + let mut list = Vec::::new(); + list_benchmarks!(list, extra); + + let storage_info = AllPalletsWithSystem::storage_info(); + + (list, storage_info) + } + + #[allow(non_local_definitions)] + fn dispatch_benchmark( + config: frame_benchmarking::BenchmarkConfig + ) -> Result, alloc::string::String> { + use frame_benchmarking::{baseline, BenchmarkBatch}; + use sp_storage::TrackedStorageKey; + use frame_system_benchmarking::Pallet as SystemBench; + use frame_system_benchmarking::extensions::Pallet as SystemExtensionsBench; + use baseline::Pallet as BaselineBench; + use super::*; + + impl frame_system_benchmarking::Config for Runtime {} + impl baseline::Config for Runtime {} + + use frame_support::traits::WhitelistedStorageKeys; + let whitelist: Vec = AllPalletsWithSystem::whitelisted_storage_keys(); + + let mut batches = Vec::::new(); + let params = (&config, &whitelist); + add_benchmarks!(params, batches); + + Ok(batches) + } + } + + #[cfg(feature = "try-runtime")] + impl frame_try_runtime::TryRuntime for Runtime { + fn on_runtime_upgrade(checks: frame_try_runtime::UpgradeCheckSelect) -> (Weight, Weight) { + // NOTE: intentional unwrap: we don't want to propagate the error backwards, and want to + // have a backtrace here. If any of the pre/post migration checks fail, we shall stop + // right here and right now. + let weight = Executive::try_runtime_upgrade(checks).unwrap(); + (weight, super::configs::RuntimeBlockWeights::get().max_block) + } + + fn execute_block( + block: Block, + state_root_check: bool, + signature_check: bool, + select: frame_try_runtime::TryStateSelect + ) -> Weight { + // NOTE: intentional unwrap: we don't want to propagate the error backwards, and want to + // have a backtrace here. + Executive::try_execute_block(block, state_root_check, signature_check, select).expect("execute-block failed") + } + } + + impl sp_genesis_builder::GenesisBuilder for Runtime { + fn build_state(config: Vec) -> sp_genesis_builder::Result { + build_state::(config) + } + + fn get_preset(id: &Option) -> Option> { + get_preset::(id, crate::genesis_config_presets::get_preset) + } + + fn preset_names() -> Vec { + crate::genesis_config_presets::preset_names() + } + } } diff --git a/runtime/src/benchmarks.rs b/runtime/src/benchmarks.rs index d3854e8..d5a79f9 100644 --- a/runtime/src/benchmarks.rs +++ b/runtime/src/benchmarks.rs @@ -24,12 +24,13 @@ // For more information, please refer to frame_benchmarking::define_benchmarks!( - [frame_benchmarking, BaselineBench::] - [frame_system, SystemBench::] - [frame_system_extensions, SystemExtensionsBench::] - [pallet_balances, Balances] - [pallet_timestamp, Timestamp] - [pallet_sudo, Sudo] - [pallet_template, Template] - [pallet_assets, Assets] + [frame_benchmarking, BaselineBench::] + [frame_system, SystemBench::] + [frame_system_extensions, SystemExtensionsBench::] + [pallet_balances, Balances] + [pallet_timestamp, Timestamp] + [pallet_sudo, Sudo] + [pallet_template, Template] + [pallet_assets, Assets] + [pallet_orderbook, Orderbook] ); diff --git a/runtime/src/configs/mod.rs b/runtime/src/configs/mod.rs index d8c5445..362a7e9 100644 --- a/runtime/src/configs/mod.rs +++ b/runtime/src/configs/mod.rs @@ -25,12 +25,12 @@ // Substrate and Polkadot dependencies use frame_support::{ - derive_impl, parameter_types, - traits::{ConstBool, ConstU128, ConstU32, ConstU64, ConstU8, VariantCountOf}, - weights::{ - constants::{RocksDbWeight, WEIGHT_REF_TIME_PER_SECOND}, - IdentityFee, Weight, - }, + derive_impl, parameter_types, + traits::{ConstBool, ConstU128, ConstU32, ConstU64, ConstU8, VariantCountOf}, + weights::{ + constants::{RocksDbWeight, WEIGHT_REF_TIME_PER_SECOND}, + IdentityFee, Weight, + }, }; use frame_system::limits::{BlockLength, BlockWeights}; use pallet_transaction_payment::{ConstFeeMultiplier, FungibleAdapter, Multiplier}; @@ -43,24 +43,24 @@ use pallet_assets; // Local module imports use super::{ - AccountId, Aura, Balance, Balances, Block, BlockNumber, Hash, Nonce, PalletInfo, Runtime, - RuntimeCall, RuntimeEvent, RuntimeFreezeReason, RuntimeHoldReason, RuntimeOrigin, RuntimeTask, - System, EXISTENTIAL_DEPOSIT, SLOT_DURATION, VERSION, + AccountId, Aura, Balance, Balances, Block, BlockNumber, Hash, Nonce, PalletInfo, Runtime, + RuntimeCall, RuntimeEvent, RuntimeFreezeReason, RuntimeHoldReason, RuntimeOrigin, RuntimeTask, + System, EXISTENTIAL_DEPOSIT, SLOT_DURATION, VERSION, }; const NORMAL_DISPATCH_RATIO: Perbill = Perbill::from_percent(75); parameter_types! { - pub const BlockHashCount: BlockNumber = 2400; - pub const Version: RuntimeVersion = VERSION; - - /// We allow for 2 seconds of compute with a 6 second average block time. - pub RuntimeBlockWeights: BlockWeights = BlockWeights::with_sensible_defaults( - Weight::from_parts(2u64 * WEIGHT_REF_TIME_PER_SECOND, u64::MAX), - NORMAL_DISPATCH_RATIO, - ); - pub RuntimeBlockLength: BlockLength = BlockLength::max_with_normal_ratio(5 * 1024 * 1024, NORMAL_DISPATCH_RATIO); - pub const SS58Prefix: u8 = 42; + pub const BlockHashCount: BlockNumber = 2400; + pub const Version: RuntimeVersion = VERSION; + + /// We allow for 2 seconds of compute with a 6 second average block time. + pub RuntimeBlockWeights: BlockWeights = BlockWeights::with_sensible_defaults( + Weight::from_parts(2u64 * WEIGHT_REF_TIME_PER_SECOND, u64::MAX), + NORMAL_DISPATCH_RATIO, + ); + pub RuntimeBlockLength: BlockLength = BlockLength::max_with_normal_ratio(5 * 1024 * 1024, NORMAL_DISPATCH_RATIO); + pub const SS58Prefix: u8 = 42; } /// The default types are being injected by [`derive_impl`](`frame_support::derive_impl`) from @@ -68,110 +68,107 @@ parameter_types! { /// but overridden as needed. #[derive_impl(frame_system::config_preludes::SolochainDefaultConfig)] impl frame_system::Config for Runtime { - /// The block type for the runtime. - type Block = Block; - /// Block & extrinsics weights: base values and limits. - type BlockWeights = RuntimeBlockWeights; - /// The maximum length of a block (in bytes). - type BlockLength = RuntimeBlockLength; - /// The identifier used to distinguish between accounts. - type AccountId = AccountId; - /// The type for storing how many extrinsics an account has signed. - type Nonce = Nonce; - /// The type for hashing blocks and tries. - type Hash = Hash; - /// Maximum number of block number to block hash mappings to keep (oldest pruned first). - type BlockHashCount = BlockHashCount; - /// The weight of database operations that the runtime can invoke. - type DbWeight = RocksDbWeight; - /// Version of the runtime. - type Version = Version; - /// The data to be stored in an account. - type AccountData = pallet_balances::AccountData; - /// This is used as an identifier of the chain. 42 is the generic substrate prefix. - type SS58Prefix = SS58Prefix; - type MaxConsumers = frame_support::traits::ConstU32<16>; + /// The block type for the runtime. + type Block = Block; + /// Block & extrinsics weights: base values and limits. + type BlockWeights = RuntimeBlockWeights; + /// The maximum length of a block (in bytes). + type BlockLength = RuntimeBlockLength; + /// The identifier used to distinguish between accounts. + type AccountId = AccountId; + /// The type for storing how many extrinsics an account has signed. + type Nonce = Nonce; + /// The type for hashing blocks and tries. + type Hash = Hash; + /// Maximum number of block number to block hash mappings to keep (oldest pruned first). + type BlockHashCount = BlockHashCount; + /// The weight of database operations that the runtime can invoke. + type DbWeight = RocksDbWeight; + /// Version of the runtime. + type Version = Version; + /// The data to be stored in an account. + type AccountData = pallet_balances::AccountData; + /// This is used as an identifier of the chain. 42 is the generic substrate prefix. + type SS58Prefix = SS58Prefix; + type MaxConsumers = frame_support::traits::ConstU32<16>; } impl pallet_aura::Config for Runtime { - type AuthorityId = AuraId; - type DisabledValidators = (); - type MaxAuthorities = ConstU32<32>; - type AllowMultipleBlocksPerSlot = ConstBool; - type SlotDuration = pallet_aura::MinimumPeriodTimesTwo; + type AuthorityId = AuraId; + type DisabledValidators = (); + type MaxAuthorities = ConstU32<32>; + type AllowMultipleBlocksPerSlot = ConstBool; + type SlotDuration = pallet_aura::MinimumPeriodTimesTwo; } impl pallet_grandpa::Config for Runtime { - type RuntimeEvent = RuntimeEvent; + type RuntimeEvent = RuntimeEvent; - type WeightInfo = (); - type MaxAuthorities = ConstU32<32>; - type MaxNominators = ConstU32<0>; - type MaxSetIdSessionEntries = ConstU64<0>; + type WeightInfo = (); + type MaxAuthorities = ConstU32<32>; + type MaxNominators = ConstU32<0>; + type MaxSetIdSessionEntries = ConstU64<0>; - type KeyOwnerProof = sp_core::Void; - type EquivocationReportSystem = (); + type KeyOwnerProof = sp_core::Void; + type EquivocationReportSystem = (); } impl pallet_timestamp::Config for Runtime { - /// A timestamp: milliseconds since the unix epoch. - type Moment = u64; - type OnTimestampSet = Aura; - type MinimumPeriod = ConstU64<{ SLOT_DURATION / 2 }>; - type WeightInfo = (); + /// A timestamp: milliseconds since the unix epoch. + type Moment = u64; + type OnTimestampSet = Aura; + type MinimumPeriod = ConstU64<{ SLOT_DURATION / 2 }>; + type WeightInfo = (); } impl pallet_balances::Config for Runtime { - type MaxLocks = ConstU32<50>; - type MaxReserves = (); - type ReserveIdentifier = [u8; 8]; - /// The type for recording an account's balance. - type Balance = Balance; - /// The ubiquitous event type. - type RuntimeEvent = RuntimeEvent; - type DustRemoval = (); - type ExistentialDeposit = ConstU128<{EXISTENTIAL_DEPOSIT}>; - type AccountStore = System; - type WeightInfo = pallet_balances::weights::SubstrateWeight; - type FreezeIdentifier = RuntimeFreezeReason; - type MaxFreezes = VariantCountOf; - type RuntimeHoldReason = RuntimeHoldReason; - type RuntimeFreezeReason = RuntimeFreezeReason; - type DoneSlashHandler = (); + type MaxLocks = ConstU32<50>; + type MaxReserves = (); + type ReserveIdentifier = [u8; 8]; + /// The type for recording an account's balance. + type Balance = Balance; + /// The ubiquitous event type. + type RuntimeEvent = RuntimeEvent; + type DustRemoval = (); + type ExistentialDeposit = ConstU128<{ EXISTENTIAL_DEPOSIT }>; + type AccountStore = System; + type WeightInfo = pallet_balances::weights::SubstrateWeight; + type FreezeIdentifier = RuntimeFreezeReason; + type MaxFreezes = VariantCountOf; + type RuntimeHoldReason = RuntimeHoldReason; + type RuntimeFreezeReason = RuntimeFreezeReason; + type DoneSlashHandler = (); } - - parameter_types! { - pub FeeMultiplier: Multiplier = Multiplier::one(); + pub FeeMultiplier: Multiplier = Multiplier::one(); } impl pallet_transaction_payment::Config for Runtime { - type RuntimeEvent = RuntimeEvent; - type OnChargeTransaction = FungibleAdapter; - type OperationalFeeMultiplier = ConstU8<5>; - type WeightToFee = IdentityFee; - type LengthToFee = IdentityFee; - type FeeMultiplierUpdate = ConstFeeMultiplier; - type WeightInfo = pallet_transaction_payment::weights::SubstrateWeight; + type RuntimeEvent = RuntimeEvent; + type OnChargeTransaction = FungibleAdapter; + type OperationalFeeMultiplier = ConstU8<5>; + type WeightToFee = IdentityFee; + type LengthToFee = IdentityFee; + type FeeMultiplierUpdate = ConstFeeMultiplier; + type WeightInfo = pallet_transaction_payment::weights::SubstrateWeight; } impl pallet_sudo::Config for Runtime { - type RuntimeEvent = RuntimeEvent; - type RuntimeCall = RuntimeCall; - type WeightInfo = pallet_sudo::weights::SubstrateWeight; + type RuntimeEvent = RuntimeEvent; + type RuntimeCall = RuntimeCall; + type WeightInfo = pallet_sudo::weights::SubstrateWeight; } /// Configure the pallet-template in pallets/template. impl pallet_template::Config for Runtime { - type RuntimeEvent = RuntimeEvent; - type WeightInfo = pallet_template::weights::SubstrateWeight; - + type RuntimeEvent = RuntimeEvent; + type WeightInfo = pallet_template::weights::SubstrateWeight; } impl pallet_assets::Config for Runtime { - type RuntimeEvent = RuntimeEvent; - type WeightInfo = pallet_assets::weights::SubstrateWeight; + type RuntimeEvent = RuntimeEvent; + type WeightInfo = pallet_assets::weights::SubstrateWeight; } parameter_types! { @@ -187,5 +184,5 @@ impl pallet_orderbook::Config for Runtime { type MaxCancellationOrders = MaxCancellationOrders; type MaxOrders = MaxOrders; type MaxUserOrders = MaxUserOrders; + type WeightInfo = pallet_orderbook::weights::SubstrateWeight; } - diff --git a/runtime/src/genesis_config_presets.rs b/runtime/src/genesis_config_presets.rs index 6af8dc9..c86daa4 100644 --- a/runtime/src/genesis_config_presets.rs +++ b/runtime/src/genesis_config_presets.rs @@ -26,84 +26,90 @@ use sp_keyring::Sr25519Keyring; // Returns the genesis config presets populated with given parameters. fn testnet_genesis( - initial_authorities: Vec<(AuraId, GrandpaId)>, - endowed_accounts: Vec, - root: AccountId, + initial_authorities: Vec<(AuraId, GrandpaId)>, + endowed_accounts: Vec, + root: AccountId, ) -> Value { - build_struct_json_patch!(RuntimeGenesisConfig { - balances: BalancesConfig { - balances: endowed_accounts - .iter() - .cloned() - .map(|k| (k, 1u128 << 60)) - .collect::>(), - }, - aura: pallet_aura::GenesisConfig { - authorities: initial_authorities.iter().map(|x| (x.0.clone())).collect::>(), - }, - grandpa: pallet_grandpa::GenesisConfig { - authorities: initial_authorities.iter().map(|x| (x.1.clone(), 1)).collect::>(), - }, - sudo: SudoConfig { key: Some(root) }, - }) + build_struct_json_patch!(RuntimeGenesisConfig { + balances: BalancesConfig { + balances: endowed_accounts + .iter() + .cloned() + .map(|k| (k, 1u128 << 60)) + .collect::>(), + }, + aura: pallet_aura::GenesisConfig { + authorities: initial_authorities + .iter() + .map(|x| (x.0.clone())) + .collect::>(), + }, + grandpa: pallet_grandpa::GenesisConfig { + authorities: initial_authorities + .iter() + .map(|x| (x.1.clone(), 1)) + .collect::>(), + }, + sudo: SudoConfig { key: Some(root) }, + }) } /// Return the development genesis config. pub fn development_config_genesis() -> Value { - testnet_genesis( - vec![( - sp_keyring::Sr25519Keyring::Alice.public().into(), - sp_keyring::Ed25519Keyring::Alice.public().into(), - )], - vec![ - Sr25519Keyring::Alice.to_account_id(), - Sr25519Keyring::Bob.to_account_id(), - Sr25519Keyring::AliceStash.to_account_id(), - Sr25519Keyring::BobStash.to_account_id(), - ], - sp_keyring::Sr25519Keyring::Alice.to_account_id(), - ) + testnet_genesis( + vec![( + sp_keyring::Sr25519Keyring::Alice.public().into(), + sp_keyring::Ed25519Keyring::Alice.public().into(), + )], + vec![ + Sr25519Keyring::Alice.to_account_id(), + Sr25519Keyring::Bob.to_account_id(), + Sr25519Keyring::AliceStash.to_account_id(), + Sr25519Keyring::BobStash.to_account_id(), + ], + sp_keyring::Sr25519Keyring::Alice.to_account_id(), + ) } /// Return the local genesis config preset. pub fn local_config_genesis() -> Value { - testnet_genesis( - vec![ - ( - sp_keyring::Sr25519Keyring::Alice.public().into(), - sp_keyring::Ed25519Keyring::Alice.public().into(), - ), - ( - sp_keyring::Sr25519Keyring::Bob.public().into(), - sp_keyring::Ed25519Keyring::Bob.public().into(), - ), - ], - Sr25519Keyring::iter() - .filter(|v| v != &Sr25519Keyring::One && v != &Sr25519Keyring::Two) - .map(|v| v.to_account_id()) - .collect::>(), - Sr25519Keyring::Alice.to_account_id(), - ) + testnet_genesis( + vec![ + ( + sp_keyring::Sr25519Keyring::Alice.public().into(), + sp_keyring::Ed25519Keyring::Alice.public().into(), + ), + ( + sp_keyring::Sr25519Keyring::Bob.public().into(), + sp_keyring::Ed25519Keyring::Bob.public().into(), + ), + ], + Sr25519Keyring::iter() + .filter(|v| v != &Sr25519Keyring::One && v != &Sr25519Keyring::Two) + .map(|v| v.to_account_id()) + .collect::>(), + Sr25519Keyring::Alice.to_account_id(), + ) } /// Provides the JSON representation of predefined genesis config for given `id`. pub fn get_preset(id: &PresetId) -> Option> { - let patch = match id.as_ref() { - sp_genesis_builder::DEV_RUNTIME_PRESET => development_config_genesis(), - sp_genesis_builder::LOCAL_TESTNET_RUNTIME_PRESET => local_config_genesis(), - _ => return None, - }; - Some( - serde_json::to_string(&patch) - .expect("serialization to json is expected to work. qed.") - .into_bytes(), - ) + let patch = match id.as_ref() { + sp_genesis_builder::DEV_RUNTIME_PRESET => development_config_genesis(), + sp_genesis_builder::LOCAL_TESTNET_RUNTIME_PRESET => local_config_genesis(), + _ => return None, + }; + Some( + serde_json::to_string(&patch) + .expect("serialization to json is expected to work. qed.") + .into_bytes(), + ) } /// List of supported presets. pub fn preset_names() -> Vec { - vec![ - PresetId::from(sp_genesis_builder::DEV_RUNTIME_PRESET), - PresetId::from(sp_genesis_builder::LOCAL_TESTNET_RUNTIME_PRESET), - ] + vec![ + PresetId::from(sp_genesis_builder::DEV_RUNTIME_PRESET), + PresetId::from(sp_genesis_builder::LOCAL_TESTNET_RUNTIME_PRESET), + ] } diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 393cc47..69d9f3e 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -11,9 +11,9 @@ pub mod configs; extern crate alloc; use alloc::vec::Vec; use sp_runtime::{ - generic, impl_opaque_keys, - traits::{BlakeTwo256, IdentifyAccount, Verify}, - MultiAddress, MultiSignature, + generic, impl_opaque_keys, + traits::{BlakeTwo256, IdentifyAccount, Verify}, + MultiAddress, MultiSignature, }; #[cfg(feature = "std")] use sp_version::NativeVersion; @@ -27,70 +27,67 @@ pub use sp_runtime::BuildStorage; pub mod genesis_config_presets; - - - /// Opaque types. These are used by the CLI to instantiate machinery that don't need to know /// the specifics of the runtime. They can then be made to be agnostic over specific formats /// of data like extrinsics, allowing for them to continue syncing the network through upgrades /// to even the core data structures. pub mod opaque { - use super::*; - use sp_runtime::{ - generic, - traits::{BlakeTwo256, Hash as HashT}, - }; - - pub use sp_runtime::OpaqueExtrinsic as UncheckedExtrinsic; - - /// Opaque block header type. - pub type Header = generic::Header; - /// Opaque block type. - pub type Block = generic::Block; - /// Opaque block identifier type. - pub type BlockId = generic::BlockId; - /// Opaque block hash type. - pub type Hash = ::Output; + use super::*; + use sp_runtime::{ + generic, + traits::{BlakeTwo256, Hash as HashT}, + }; + + pub use sp_runtime::OpaqueExtrinsic as UncheckedExtrinsic; + + /// Opaque block header type. + pub type Header = generic::Header; + /// Opaque block type. + pub type Block = generic::Block; + /// Opaque block identifier type. + pub type BlockId = generic::BlockId; + /// Opaque block hash type. + pub type Hash = ::Output; } impl_opaque_keys! { - pub struct SessionKeys { - pub aura: Aura, - pub grandpa: Grandpa, - } + pub struct SessionKeys { + pub aura: Aura, + pub grandpa: Grandpa, + } } // To learn more about runtime versioning, see: // https://docs.substrate.io/main-docs/build/upgrade#runtime-versioning #[sp_version::runtime_version] pub const VERSION: RuntimeVersion = RuntimeVersion { - spec_name: alloc::borrow::Cow::Borrowed("solochain-template-runtime"), - impl_name: alloc::borrow::Cow::Borrowed("solochain-template-runtime"), - authoring_version: 1, - // The version of the runtime specification. A full node will not attempt to use its native - // runtime in substitute for the on-chain Wasm runtime unless all of `spec_name`, - // `spec_version`, and `authoring_version` are the same between Wasm and native. - // This value is set to 100 to notify Polkadot-JS App (https://polkadot.js.org/apps) to use - // the compatible custom types. - spec_version: 100, - impl_version: 1, - apis: apis::RUNTIME_API_VERSIONS, - transaction_version: 1, - system_version: 1, + spec_name: alloc::borrow::Cow::Borrowed("solochain-template-runtime"), + impl_name: alloc::borrow::Cow::Borrowed("solochain-template-runtime"), + authoring_version: 1, + // The version of the runtime specification. A full node will not attempt to use its native + // runtime in substitute for the on-chain Wasm runtime unless all of `spec_name`, + // `spec_version`, and `authoring_version` are the same between Wasm and native. + // This value is set to 100 to notify Polkadot-JS App (https://polkadot.js.org/apps) to use + // the compatible custom types. + spec_version: 100, + impl_version: 1, + apis: apis::RUNTIME_API_VERSIONS, + transaction_version: 1, + system_version: 1, }; mod block_times { - /// This determines the average expected block time that we are targeting. Blocks will be - /// produced at a minimum duration defined by `SLOT_DURATION`. `SLOT_DURATION` is picked up by - /// `pallet_timestamp` which is in turn picked up by `pallet_aura` to implement `fn - /// slot_duration()`. - /// - /// Change this to adjust the block time. - pub const MILLI_SECS_PER_BLOCK: u64 = 6000; - - // NOTE: Currently it is not possible to change the slot duration after the chain has started. - // Attempting to do so will brick block production. - pub const SLOT_DURATION: u64 = MILLI_SECS_PER_BLOCK; + /// This determines the average expected block time that we are targeting. Blocks will be + /// produced at a minimum duration defined by `SLOT_DURATION`. `SLOT_DURATION` is picked up by + /// `pallet_timestamp` which is in turn picked up by `pallet_aura` to implement `fn + /// slot_duration()`. + /// + /// Change this to adjust the block time. + pub const MILLI_SECS_PER_BLOCK: u64 = 6000; + + // NOTE: Currently it is not possible to change the slot duration after the chain has started. + // Attempting to do so will brick block production. + pub const SLOT_DURATION: u64 = MILLI_SECS_PER_BLOCK; } pub use block_times::*; @@ -112,7 +109,10 @@ pub const EXISTENTIAL_DEPOSIT: Balance = MILLI_UNIT; /// The version information used to identify this runtime when compiled natively. #[cfg(feature = "std")] pub fn native_version() -> NativeVersion { - NativeVersion { runtime_version: VERSION, can_author_with: Default::default() } + NativeVersion { + runtime_version: VERSION, + can_author_with: Default::default(), + } } /// Alias to 512-bit hash when used in the context of a transaction signature on the chain. @@ -151,21 +151,21 @@ pub type BlockId = generic::BlockId; /// The `TransactionExtension` to the basic transaction logic. pub type TxExtension = ( - frame_system::CheckNonZeroSender, - frame_system::CheckSpecVersion, - frame_system::CheckTxVersion, - frame_system::CheckGenesis, - frame_system::CheckEra, - frame_system::CheckNonce, - frame_system::CheckWeight, - pallet_transaction_payment::ChargeTransactionPayment, - frame_metadata_hash_extension::CheckMetadataHash, - frame_system::WeightReclaim, + frame_system::CheckNonZeroSender, + frame_system::CheckSpecVersion, + frame_system::CheckTxVersion, + frame_system::CheckGenesis, + frame_system::CheckEra, + frame_system::CheckNonce, + frame_system::CheckWeight, + pallet_transaction_payment::ChargeTransactionPayment, + frame_metadata_hash_extension::CheckMetadataHash, + frame_system::WeightReclaim, ); /// Unchecked extrinsic type as expected by this runtime. pub type UncheckedExtrinsic = - generic::UncheckedExtrinsic; + generic::UncheckedExtrinsic; /// The payload being signed in transactions. pub type SignedPayload = generic::SignedPayload; @@ -178,60 +178,60 @@ type Migrations = (); /// Executive: handles dispatch to the various modules. pub type Executive = frame_executive::Executive< - Runtime, - Block, - frame_system::ChainContext, - Runtime, - AllPalletsWithSystem, - Migrations, + Runtime, + Block, + frame_system::ChainContext, + Runtime, + AllPalletsWithSystem, + Migrations, >; // Create the runtime by composing the FRAME pallets that were previously configured. #[frame_support::runtime] mod runtime { - #[runtime::runtime] - #[runtime::derive( - RuntimeCall, - RuntimeEvent, - RuntimeError, - RuntimeOrigin, - RuntimeFreezeReason, - RuntimeHoldReason, - RuntimeSlashReason, - RuntimeLockId, - RuntimeTask, - RuntimeViewFunction - )] - pub struct Runtime; - - #[runtime::pallet_index(0)] - pub type System = frame_system; - - #[runtime::pallet_index(1)] - pub type Timestamp = pallet_timestamp; - - #[runtime::pallet_index(2)] - pub type Aura = pallet_aura; - - #[runtime::pallet_index(3)] - pub type Grandpa = pallet_grandpa; - - #[runtime::pallet_index(4)] - pub type Balances = pallet_balances; - - #[runtime::pallet_index(5)] - pub type TransactionPayment = pallet_transaction_payment; - - #[runtime::pallet_index(6)] - pub type Sudo = pallet_sudo; - - // Include the custom logic from the pallet-template in the runtime. - #[runtime::pallet_index(7)] - pub type Template = pallet_template; - - #[runtime::pallet_index(8)] - pub type Assets = pallet_assets; - - #[runtime::pallet_index(9)] - pub type Orderbook = pallet_orderbook; -} \ No newline at end of file + #[runtime::runtime] + #[runtime::derive( + RuntimeCall, + RuntimeEvent, + RuntimeError, + RuntimeOrigin, + RuntimeFreezeReason, + RuntimeHoldReason, + RuntimeSlashReason, + RuntimeLockId, + RuntimeTask, + RuntimeViewFunction + )] + pub struct Runtime; + + #[runtime::pallet_index(0)] + pub type System = frame_system; + + #[runtime::pallet_index(1)] + pub type Timestamp = pallet_timestamp; + + #[runtime::pallet_index(2)] + pub type Aura = pallet_aura; + + #[runtime::pallet_index(3)] + pub type Grandpa = pallet_grandpa; + + #[runtime::pallet_index(4)] + pub type Balances = pallet_balances; + + #[runtime::pallet_index(5)] + pub type TransactionPayment = pallet_transaction_payment; + + #[runtime::pallet_index(6)] + pub type Sudo = pallet_sudo; + + // Include the custom logic from the pallet-template in the runtime. + #[runtime::pallet_index(7)] + pub type Template = pallet_template; + + #[runtime::pallet_index(8)] + pub type Assets = pallet_assets; + + #[runtime::pallet_index(9)] + pub type Orderbook = pallet_orderbook; +} From ad9516bd710cf9fdb67d81ac102b6afde5dd6743 Mon Sep 17 00:00:00 2001 From: gil7788 Date: Tue, 21 Oct 2025 10:57:25 +0300 Subject: [PATCH 08/15] Init Frontend --- frontend/.dockerignore | 36 + frontend/.gitignore | 27 + frontend/Dockerfile | 31 + frontend/README.docker.md | 62 + frontend/app/globals.css | 126 + frontend/app/layout.tsx | 28 + frontend/app/page.tsx | 9 + frontend/components.json | 21 + frontend/components/account-tabs.tsx | 178 + frontend/components/market-stats.tsx | 95 + frontend/components/order-book.tsx | 196 + frontend/components/providers.tsx | 30 + frontend/components/theme-provider.tsx | 7 + frontend/components/theme-toggle.tsx | 22 + frontend/components/trading-dashboard.tsx | 151 + frontend/components/trading-form.tsx | 207 + frontend/components/trading-view-chart.tsx | 55 + frontend/components/ui/accordion.tsx | 66 + frontend/components/ui/alert-dialog.tsx | 157 + frontend/components/ui/alert.tsx | 66 + frontend/components/ui/aspect-ratio.tsx | 11 + frontend/components/ui/avatar.tsx | 53 + frontend/components/ui/badge.tsx | 46 + frontend/components/ui/breadcrumb.tsx | 109 + frontend/components/ui/button-group.tsx | 83 + frontend/components/ui/button.tsx | 60 + frontend/components/ui/calendar.tsx | 213 + frontend/components/ui/card.tsx | 92 + frontend/components/ui/carousel.tsx | 241 + frontend/components/ui/chart.tsx | 353 + frontend/components/ui/checkbox.tsx | 32 + frontend/components/ui/collapsible.tsx | 33 + frontend/components/ui/command.tsx | 184 + frontend/components/ui/context-menu.tsx | 252 + frontend/components/ui/dialog.tsx | 143 + frontend/components/ui/drawer.tsx | 135 + frontend/components/ui/dropdown-menu.tsx | 257 + frontend/components/ui/empty.tsx | 104 + frontend/components/ui/field.tsx | 244 + frontend/components/ui/form.tsx | 167 + frontend/components/ui/hover-card.tsx | 44 + frontend/components/ui/input-group.tsx | 169 + frontend/components/ui/input-otp.tsx | 77 + frontend/components/ui/input.tsx | 21 + frontend/components/ui/item.tsx | 193 + frontend/components/ui/kbd.tsx | 28 + frontend/components/ui/label.tsx | 24 + frontend/components/ui/menubar.tsx | 276 + frontend/components/ui/navigation-menu.tsx | 166 + frontend/components/ui/pagination.tsx | 127 + frontend/components/ui/popover.tsx | 48 + frontend/components/ui/progress.tsx | 31 + frontend/components/ui/radio-group.tsx | 45 + frontend/components/ui/resizable.tsx | 56 + frontend/components/ui/scroll-area.tsx | 58 + frontend/components/ui/select.tsx | 185 + frontend/components/ui/separator.tsx | 28 + frontend/components/ui/sheet.tsx | 139 + frontend/components/ui/sidebar.tsx | 726 +++ frontend/components/ui/skeleton.tsx | 13 + frontend/components/ui/slider.tsx | 63 + frontend/components/ui/sonner.tsx | 25 + frontend/components/ui/spinner.tsx | 16 + frontend/components/ui/switch.tsx | 31 + frontend/components/ui/table.tsx | 116 + frontend/components/ui/tabs.tsx | 66 + frontend/components/ui/textarea.tsx | 18 + frontend/components/ui/toast.tsx | 129 + frontend/components/ui/toaster.tsx | 35 + frontend/components/ui/toggle-group.tsx | 73 + frontend/components/ui/toggle.tsx | 47 + frontend/components/ui/tooltip.tsx | 61 + frontend/components/ui/use-mobile.tsx | 19 + frontend/components/ui/use-toast.ts | 191 + frontend/components/wallet-connect.tsx | 93 + frontend/docker-compose.yml | 9 + frontend/hooks/use-balances.ts | 46 + frontend/hooks/use-market-stats.ts | 44 + frontend/hooks/use-mobile.ts | 19 + frontend/hooks/use-open-orders.ts | 55 + frontend/hooks/use-order-book.ts | 77 + frontend/hooks/use-positions.ts | 60 + frontend/hooks/use-toast.ts | 191 + frontend/hooks/use-trades.ts | 49 + frontend/instrumentation.ts | 43 + frontend/lib/env.ts | 50 + frontend/lib/utils.ts | 6 + frontend/lib/wagmi-config.ts | 17 + frontend/next.config.mjs | 30 + frontend/package.json | 76 + frontend/pnpm-lock.yaml | 6861 ++++++++++++++++++++ frontend/postcss.config.mjs | 8 + frontend/public/placeholder-logo.png | Bin 0 -> 568 bytes frontend/public/placeholder-logo.svg | 1 + frontend/public/placeholder-user.jpg | Bin 0 -> 1635 bytes frontend/public/placeholder.jpg | Bin 0 -> 1064 bytes frontend/public/placeholder.svg | 1 + frontend/styles/globals.css | 125 + frontend/tsconfig.json | 27 + 99 files changed, 15614 insertions(+) create mode 100644 frontend/.dockerignore create mode 100644 frontend/.gitignore create mode 100644 frontend/Dockerfile create mode 100644 frontend/README.docker.md create mode 100644 frontend/app/globals.css create mode 100644 frontend/app/layout.tsx create mode 100644 frontend/app/page.tsx create mode 100644 frontend/components.json create mode 100644 frontend/components/account-tabs.tsx create mode 100644 frontend/components/market-stats.tsx create mode 100644 frontend/components/order-book.tsx create mode 100644 frontend/components/providers.tsx create mode 100644 frontend/components/theme-provider.tsx create mode 100644 frontend/components/theme-toggle.tsx create mode 100644 frontend/components/trading-dashboard.tsx create mode 100644 frontend/components/trading-form.tsx create mode 100644 frontend/components/trading-view-chart.tsx create mode 100644 frontend/components/ui/accordion.tsx create mode 100644 frontend/components/ui/alert-dialog.tsx create mode 100644 frontend/components/ui/alert.tsx create mode 100644 frontend/components/ui/aspect-ratio.tsx create mode 100644 frontend/components/ui/avatar.tsx create mode 100644 frontend/components/ui/badge.tsx create mode 100644 frontend/components/ui/breadcrumb.tsx create mode 100644 frontend/components/ui/button-group.tsx create mode 100644 frontend/components/ui/button.tsx create mode 100644 frontend/components/ui/calendar.tsx create mode 100644 frontend/components/ui/card.tsx create mode 100644 frontend/components/ui/carousel.tsx create mode 100644 frontend/components/ui/chart.tsx create mode 100644 frontend/components/ui/checkbox.tsx create mode 100644 frontend/components/ui/collapsible.tsx create mode 100644 frontend/components/ui/command.tsx create mode 100644 frontend/components/ui/context-menu.tsx create mode 100644 frontend/components/ui/dialog.tsx create mode 100644 frontend/components/ui/drawer.tsx create mode 100644 frontend/components/ui/dropdown-menu.tsx create mode 100644 frontend/components/ui/empty.tsx create mode 100644 frontend/components/ui/field.tsx create mode 100644 frontend/components/ui/form.tsx create mode 100644 frontend/components/ui/hover-card.tsx create mode 100644 frontend/components/ui/input-group.tsx create mode 100644 frontend/components/ui/input-otp.tsx create mode 100644 frontend/components/ui/input.tsx create mode 100644 frontend/components/ui/item.tsx create mode 100644 frontend/components/ui/kbd.tsx create mode 100644 frontend/components/ui/label.tsx create mode 100644 frontend/components/ui/menubar.tsx create mode 100644 frontend/components/ui/navigation-menu.tsx create mode 100644 frontend/components/ui/pagination.tsx create mode 100644 frontend/components/ui/popover.tsx create mode 100644 frontend/components/ui/progress.tsx create mode 100644 frontend/components/ui/radio-group.tsx create mode 100644 frontend/components/ui/resizable.tsx create mode 100644 frontend/components/ui/scroll-area.tsx create mode 100644 frontend/components/ui/select.tsx create mode 100644 frontend/components/ui/separator.tsx create mode 100644 frontend/components/ui/sheet.tsx create mode 100644 frontend/components/ui/sidebar.tsx create mode 100644 frontend/components/ui/skeleton.tsx create mode 100644 frontend/components/ui/slider.tsx create mode 100644 frontend/components/ui/sonner.tsx create mode 100644 frontend/components/ui/spinner.tsx create mode 100644 frontend/components/ui/switch.tsx create mode 100644 frontend/components/ui/table.tsx create mode 100644 frontend/components/ui/tabs.tsx create mode 100644 frontend/components/ui/textarea.tsx create mode 100644 frontend/components/ui/toast.tsx create mode 100644 frontend/components/ui/toaster.tsx create mode 100644 frontend/components/ui/toggle-group.tsx create mode 100644 frontend/components/ui/toggle.tsx create mode 100644 frontend/components/ui/tooltip.tsx create mode 100644 frontend/components/ui/use-mobile.tsx create mode 100644 frontend/components/ui/use-toast.ts create mode 100644 frontend/components/wallet-connect.tsx create mode 100644 frontend/docker-compose.yml create mode 100644 frontend/hooks/use-balances.ts create mode 100644 frontend/hooks/use-market-stats.ts create mode 100644 frontend/hooks/use-mobile.ts create mode 100644 frontend/hooks/use-open-orders.ts create mode 100644 frontend/hooks/use-order-book.ts create mode 100644 frontend/hooks/use-positions.ts create mode 100644 frontend/hooks/use-toast.ts create mode 100644 frontend/hooks/use-trades.ts create mode 100644 frontend/instrumentation.ts create mode 100644 frontend/lib/env.ts create mode 100644 frontend/lib/utils.ts create mode 100644 frontend/lib/wagmi-config.ts create mode 100644 frontend/next.config.mjs create mode 100644 frontend/package.json create mode 100644 frontend/pnpm-lock.yaml create mode 100644 frontend/postcss.config.mjs create mode 100644 frontend/public/placeholder-logo.png create mode 100644 frontend/public/placeholder-logo.svg create mode 100644 frontend/public/placeholder-user.jpg create mode 100644 frontend/public/placeholder.jpg create mode 100644 frontend/public/placeholder.svg create mode 100644 frontend/styles/globals.css create mode 100644 frontend/tsconfig.json diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 0000000..f217141 --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,36 @@ +# Dependencies +node_modules +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Next.js +.next +out +build + +# Environment files +.env*.local +.env + +# Git +.git +.gitignore + +# IDE +.vscode +.idea +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Testing +coverage +.nyc_output + +# Misc +*.log +.cache diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..f650315 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,27 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules + +# next.js +/.next/ +/out/ + +# production +/build + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts \ No newline at end of file diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..5e3e030 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,31 @@ +# ----- base ----- +FROM node:20-alpine AS base +WORKDIR /app +RUN apk add --no-cache libc6-compat && corepack enable + +# Only lockfile + manifest first for better caching +COPY package.json pnpm-lock.yaml ./ +RUN corepack prepare pnpm@9 --activate && pnpm fetch + +# ----- build ----- +FROM base AS build +COPY . . +# If you removed the workspace file, this installs just this app +RUN pnpm install --no-frozen-lockfile +RUN pnpm build + +# ----- runtime ----- +FROM node:20-alpine AS runtime +WORKDIR /app +RUN addgroup -S nextjs && adduser -S nextjs -G nextjs + +# Copy minimal runtime artifacts +COPY --from=build /app/.next ./.next +COPY --from=build /app/public ./public +COPY --from=build /app/package.json . +COPY --from=build /app/node_modules ./node_modules +COPY --from=build /app/next.config.mjs ./next.config.mjs + +EXPOSE 3000 +USER nextjs +CMD ["node", "node_modules/next/dist/bin/next", "start", "-p", "3000"] diff --git a/frontend/README.docker.md b/frontend/README.docker.md new file mode 100644 index 0000000..2fee2c2 --- /dev/null +++ b/frontend/README.docker.md @@ -0,0 +1,62 @@ +# Docker Setup for Orbex + +## Prerequisites +- Docker installed on your system +- Docker Compose installed + +## Environment Variables + +The application requires the following environment variables: +- `NODE_ENV`: Set to `prod` for production +- `SERVER_URL`: API server URL (default: http://localhost:3000) +- `NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID`: Your WalletConnect project ID + +## Quick Start + +### 1. Build and run with Docker Compose + +\`\`\`bash +docker-compose up --build +\`\`\` + +The application will be available at http://localhost:3000 + +### 2. Run in detached mode + +\`\`\`bash +docker-compose up -d +\`\`\` + +### 3. Stop the application + +\`\`\`bash +docker-compose down +\`\`\` + +## Manual Docker Commands + +### Build the image + +\`\`\`bash +docker build -t orbex-web-app . +\`\`\` + +### Run the container + +\`\`\`bash +docker run -p 3000:3000 \ + -e NODE_ENV=prod \ + -e SERVER_URL=http://localhost:3000 \ + -e NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=your-project-id \ + orbex-web-app +\`\`\` + +## Development + +To override environment variables, create a `.env` file or modify the `docker-compose.yml` file. + +## Troubleshooting + +- If port 3000 is already in use, modify the port mapping in `docker-compose.yml` +- Check logs: `docker-compose logs -f` +- Rebuild after code changes: `docker-compose up --build` diff --git a/frontend/app/globals.css b/frontend/app/globals.css new file mode 100644 index 0000000..29a1e34 --- /dev/null +++ b/frontend/app/globals.css @@ -0,0 +1,126 @@ +@import "tailwindcss"; +@import "tw-animate-css"; + +@custom-variant dark (&:is(.dark *)); + +:root { + --background: hsl(0 0% 100%); + --foreground: hsl(215 25% 18%); + --card: hsl(0 0% 100%); + --card-foreground: oklch(0.145 0 0); + --popover: hsl(0 0% 100%); + --popover-foreground: oklch(0.145 0 0); + --primary: hsl(215 45% 32%); + --primary-foreground: oklch(1 0 0); + --secondary: hsl(215 15% 93%); + --secondary-foreground: oklch(0.145 0 0); + --muted: hsl(215 15% 93%); + --muted-foreground: oklch(0.5 0 0); + --accent: hsl(215 25% 85%); + --accent-foreground: oklch(1 0 0); + --destructive: hsl(0 84.2% 60.2%); + --destructive-foreground: oklch(1 0 0); + --border: hsl(215 18% 88%); + --input: hsl(215 18% 88%); + --ring: hsl(215 45% 32%); + --chart-1: hsl(215 45% 32%); + --chart-2: hsl(220 38% 45%); + --chart-3: hsl(210 32% 58%); + --chart-4: hsl(205 25% 68%); + --chart-5: hsl(218 52% 22%); + --radius: 0.5rem; + --sidebar: hsl(215 12% 97%); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); +} + +.dark { + --background: hsl(222.2 84% 4.9%); + --foreground: hsl(210 40% 98%); + --card: hsl(222.2 84% 4.9%); + --card-foreground: oklch(0.98 0 0); + --popover: hsl(222.2 84% 4.9%); + --popover-foreground: oklch(0.98 0 0); + --primary: hsl(210 40% 98%); + --primary-foreground: oklch(0.12 0 0); + --secondary: hsl(217.2 32.6% 17.5%); + --secondary-foreground: oklch(0.98 0 0); + --muted: hsl(217.2 32.6% 17.5%); + --muted-foreground: oklch(0.65 0 0); + --accent: hsl(217.2 32.6% 17.5%); + --accent-foreground: oklch(0.98 0 0); + --destructive: hsl(0 62.8% 30.6%); + --destructive-foreground: oklch(1 0 0); + --border: hsl(217.2 32.6% 17.5%); + --input: hsl(217.2 32.6% 17.5%); + --ring: hsl(212.7 26.8% 83.9%); + --chart-1: hsl(220 70% 50%); + --chart-2: hsl(160 60% 45%); + --chart-3: hsl(30 80% 55%); + --chart-4: hsl(280 65% 60%); + --chart-5: hsl(340 75% 55%); + --sidebar: hsl(240 5.9% 10%); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(0.269 0 0); + --sidebar-ring: oklch(0.439 0 0); +} + +@theme inline { + /* optional: --font-sans, --font-serif, --font-mono if they are applied in the layout.tsx */ + --font-sans: "Geist", "Geist Fallback"; + --font-mono: "Geist Mono", "Geist Mono Fallback"; + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx new file mode 100644 index 0000000..095acf2 --- /dev/null +++ b/frontend/app/layout.tsx @@ -0,0 +1,28 @@ +import type React from 'react' +import type { Metadata } from 'next' +import { Geist, Geist_Mono } from 'next/font/google' +import './globals.css' +import { ThemeProvider } from '@/components/theme-provider' + +// โœ… import the client component directly +import { Providers } from '@/components/providers' + +const geistSans = Geist({ variable: '--font-geist-sans', subsets: ['latin'] }) +const geistMono = Geist_Mono({ variable: '--font-geist-mono', subsets: ['latin'] }) + +export const metadata: Metadata = { + title: 'Orbex', + description: 'Modern crypto trading platform', +} + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + + + {children} + + + + ) +} diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx new file mode 100644 index 0000000..8d4dc0d --- /dev/null +++ b/frontend/app/page.tsx @@ -0,0 +1,9 @@ +import { TradingDashboard } from "@/components/trading-dashboard" + +export default function Home() { + return ( +

+ +
+ ) +} diff --git a/frontend/components.json b/frontend/components.json new file mode 100644 index 0000000..4ee62ee --- /dev/null +++ b/frontend/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} diff --git a/frontend/components/account-tabs.tsx b/frontend/components/account-tabs.tsx new file mode 100644 index 0000000..db8a58a --- /dev/null +++ b/frontend/components/account-tabs.tsx @@ -0,0 +1,178 @@ +"use client" + +import { useState } from "react" +import { useBalances } from "@/hooks/use-balances" +import { usePositions } from "@/hooks/use-positions" +import { useOpenOrders } from "@/hooks/use-open-orders" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { Card, CardContent } from "@/components/ui/card" + +export function AccountTabs() { + const [activeTab, setActiveTab] = useState("balances") + + const balances = useBalances() + const positions = usePositions() + const openOrders = useOpenOrders() + + return ( + + + + + + Balances + + + Positions + + + Open Orders + + + TWAP + + + Trade History + + + Funding History + + + Order History + + + + +
+ + + + + + + + + + + {balances.map((balance) => ( + + + + + + + ))} + +
AssetAmountAvailableValue
{balance.asset}{balance.amount}{balance.available}{balance.value}
+
+
+ + +
+ + + + + + + + + + + + + {positions.map((position, idx) => ( + + + + + + + + + ))} + +
PairSideSizeEntry PriceMark PricePnL
{position.pair} + + {position.side} + + {position.size}{position.entryPrice}{position.markPrice} + {position.pnl} ({position.pnlPercent}) +
+
+
+ + +
+ + + + + + + + + + + + + + {openOrders.map((order, idx) => ( + + + + + + + + + + ))} + +
PairTypeSidePriceAmountFilledTotal
{order.pair}{order.type} + {order.side} + {order.price}{order.amount}{order.filled}{order.total}
+
+
+ + +
No TWAP orders
+
+ + +
No trade history
+
+ + +
No funding history
+
+ + +
No order history
+
+
+
+
+ ) +} diff --git a/frontend/components/market-stats.tsx b/frontend/components/market-stats.tsx new file mode 100644 index 0000000..0b249c4 --- /dev/null +++ b/frontend/components/market-stats.tsx @@ -0,0 +1,95 @@ +"use client" + +import { Card, CardContent } from "@/components/ui/card" +import { TrendingUp, TrendingDown } from "lucide-react" + +interface MarketStatsProps { + selectedPair: string +} + +export function MarketStats({ selectedPair }: MarketStatsProps) { + // Mock data - in production, fetch from API + const stats = { + "BTC/USD": { + price: "67,234.50", + change: "+2.34", + changePercent: "+3.61%", + high24h: "68,450.00", + low24h: "65,120.00", + volume24h: "28.5B", + isPositive: true, + }, + "ETH/USD": { + price: "3,456.78", + change: "-45.23", + changePercent: "-1.29%", + high24h: "3,520.00", + low24h: "3,401.00", + volume24h: "12.3B", + isPositive: false, + }, + "SOL/USD": { + price: "142.56", + change: "+8.92", + changePercent: "+6.68%", + high24h: "145.00", + low24h: "135.20", + volume24h: "2.1B", + isPositive: true, + }, + } + + const currentStats = stats[selectedPair as keyof typeof stats] + + return ( +
+ + +
+

Price

+
+

${currentStats.price}

+
+ {currentStats.isPositive ? : } + {currentStats.changePercent} +
+
+
+
+
+ + + +
+

24h Change

+

+ ${currentStats.change} +

+
+
+
+ + + +
+

24h High

+

${currentStats.high24h}

+
+
+
+ + + +
+

24h Volume

+

${currentStats.volume24h}

+
+
+
+
+ ) +} diff --git a/frontend/components/order-book.tsx b/frontend/components/order-book.tsx new file mode 100644 index 0000000..122f300 --- /dev/null +++ b/frontend/components/order-book.tsx @@ -0,0 +1,196 @@ +"use client" + +import { useState, useEffect, useRef } from "react" +import { useOrderBook } from "@/hooks/use-order-book" +import { useTrades } from "@/hooks/use-trades" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" + +function OrderBookRow({ + price, + size, + total, + sizeNum, + maxSize, + type, + priceGrouping, +}: { + price: string + size: string + total: string + sizeNum: number + maxSize: number + type: "ask" | "bid" + priceGrouping: number +}) { + const [flash, setFlash] = useState(false) + const prevSizeRef = useRef(sizeNum) + + useEffect(() => { + // Detect size changes and trigger flash animation + if (prevSizeRef.current !== sizeNum) { + setFlash(true) + const timer = setTimeout(() => setFlash(false), 300) + prevSizeRef.current = sizeNum + return () => clearTimeout(timer) + } + }, [sizeNum]) + + // Calculate heatmap intensity (0-1) + const intensity = maxSize > 0 ? sizeNum / maxSize : 0 + + const bgGradient = + type === "ask" + ? `linear-gradient(to right, transparent ${100 - intensity * 100}%, rgba(239, 68, 68, ${intensity * 0.3}) ${100 - intensity * 100}%)` + : `linear-gradient(to right, transparent ${100 - intensity * 100}%, rgba(34, 197, 94, ${intensity * 0.3}) ${100 - intensity * 100}%)` + + const formatPrice = (priceStr: string, grouping: number): string => { + const priceNum = Number(priceStr) + + if (grouping >= 1) { + // Round up to nearest multiple of grouping + const rounded = Math.ceil(priceNum / grouping) * grouping + return rounded.toFixed(0) + } else { + // For decimal groupings, use decimal places + const decimals = Math.max(0, -Math.log10(grouping)) + return priceNum.toFixed(decimals) + } + } + + const formattedPrice = formatPrice(price, priceGrouping) + + return ( +
+
{formattedPrice}
+
{size}
+
{total}
+
+ ) +} + +export function OrderBook() { + const [activeTab, setActiveTab] = useState<"orderbook" | "trades">("orderbook") + const [priceGrouping, setPriceGrouping] = useState(0.01) + + const { asks, bids, spread, spreadPercent, maxSize } = useOrderBook() + const trades = useTrades() + + return ( +
+
+ + +
+ + {activeTab === "orderbook" ? ( + <> +
+ +
+ +
+
Price
+
+ Size ETH +
+
Total
+
+ +
+
+ {asks.map((ask, i) => ( + + ))} +
+ +
+
+ {spread} + Spread + {spreadPercent}% +
+
+ +
+ {bids.map((bid, i) => ( + + ))} +
+
+ + ) : ( + <> +
+
Price
+
Size
+
Time
+
+ +
+ {trades.map((trade, i) => ( +
+
{trade.price}
+
{trade.size}
+
{trade.time}
+
+ ))} +
+ + )} +
+ ) +} diff --git a/frontend/components/providers.tsx b/frontend/components/providers.tsx new file mode 100644 index 0000000..9e4aabd --- /dev/null +++ b/frontend/components/providers.tsx @@ -0,0 +1,30 @@ +'use client' + +import type React from 'react' +import { useMemo, useState } from 'react' + +import { WagmiProvider, createConfig, http } from 'wagmi' +import { mainnet } from 'wagmi/chains' +// IMPORTANT: use injected() instead of metaMask() to avoid pulling @metamask/sdk +import { injected } from 'wagmi/connectors' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' + +export function Providers({ children }: { children: React.ReactNode }) { + const [queryClient] = useState(() => new QueryClient()) + + // Create config on the client; injected() does not touch indexedDB/RN storage. + const config = useMemo(() => { + return createConfig({ + chains: [mainnet], + transports: { [mainnet.id]: http() }, + connectors: [injected()], + // Do NOT enable any persistence layer that uses indexedDB on module load. + }) + }, []) + + return ( + + {children} + + ) +} diff --git a/frontend/components/theme-provider.tsx b/frontend/components/theme-provider.tsx new file mode 100644 index 0000000..1cd216d --- /dev/null +++ b/frontend/components/theme-provider.tsx @@ -0,0 +1,7 @@ +"use client" +import { ThemeProvider as NextThemesProvider } from "next-themes" +import type { ThemeProviderProps } from "next-themes" + +export function ThemeProvider({ children, ...props }: ThemeProviderProps) { + return {children} +} diff --git a/frontend/components/theme-toggle.tsx b/frontend/components/theme-toggle.tsx new file mode 100644 index 0000000..1ed9cbf --- /dev/null +++ b/frontend/components/theme-toggle.tsx @@ -0,0 +1,22 @@ +"use client" +import { Moon, Sun } from "lucide-react" +import { useTheme } from "next-themes" + +import { Button } from "@/components/ui/button" + +export function ThemeToggle() { + const { theme, setTheme } = useTheme() + + return ( + + ) +} diff --git a/frontend/components/trading-dashboard.tsx b/frontend/components/trading-dashboard.tsx new file mode 100644 index 0000000..a8ebfbc --- /dev/null +++ b/frontend/components/trading-dashboard.tsx @@ -0,0 +1,151 @@ +"use client" + +import { useState } from "react" +import { Button } from "@/components/ui/button" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import { TradingViewChart } from "@/components/trading-view-chart" +import { WalletConnect } from "@/components/wallet-connect" +import { OrderBook } from "@/components/order-book" +import { TradingForm } from "@/components/trading-form" +import { AccountTabs } from "@/components/account-tabs" +import { ThemeToggle } from "@/components/theme-toggle" +import { Activity, TrendingUp, TrendingDown } from "lucide-react" + +export function TradingDashboard() { + const [selectedPair, setSelectedPair] = useState("BTC/USD") + + const stats = { + "BTC/USD": { + price: "67,234.50", + change: "+2.34", + changePercent: "+3.61%", + high24h: "68,450.00", + low24h: "65,120.00", + volume24h: "28.5B", + isPositive: true, + }, + "ETH/USD": { + price: "3,456.78", + change: "-45.23", + changePercent: "-1.29%", + high24h: "3,520.00", + low24h: "3,401.00", + volume24h: "12.3B", + isPositive: false, + }, + "SOL/USD": { + price: "142.56", + change: "+8.92", + changePercent: "+6.68%", + high24h: "145.00", + low24h: "135.20", + volume24h: "2.1B", + isPositive: true, + }, + } + + const currentStats = stats[selectedPair as keyof typeof stats] + + return ( +
+ {/* Header */} +
+
+
+
+ +

Orbex

+
+ +
+
+ + +
+
+
+ + {/* Main Content */} +
+ {/* Middle Section - Chart, Order Book, and Trading Form */} +
+ {/* Left Section - Chart and Order Book */} +
+ {/* Chart and Order Book Row */} +
+ {/* Trading Chart */} +
+
+
+ +
+ ${currentStats.price} +
+ {currentStats.isPositive ? ( + + ) : ( + + )} + {currentStats.changePercent} +
+
+
+
+ + +
+
+
+ +
+
+ + {/* Order Book */} +
+ +
+
+ +
+ +
+
+ + {/* Right Column - Trading Form */} +
+ +
+
+
+
+ ) +} diff --git a/frontend/components/trading-form.tsx b/frontend/components/trading-form.tsx new file mode 100644 index 0000000..2bb2df0 --- /dev/null +++ b/frontend/components/trading-form.tsx @@ -0,0 +1,207 @@ +"use client" + +import { useState } from "react" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { Slider } from "@/components/ui/slider" +import { useToast } from "@/hooks/use-toast" + +interface TradingFormProps { + selectedPair: string +} + +export function TradingForm({ selectedPair }: TradingFormProps) { + const [buyAmount, setBuyAmount] = useState("") + const [buyPrice, setBuyPrice] = useState("") + const [sellAmount, setSellAmount] = useState("") + const [sellPrice, setSellPrice] = useState("") + const [buyPercentage, setBuyPercentage] = useState([0]) + const [sellPercentage, setSellPercentage] = useState([0]) + const { toast } = useToast() + + const handleBuy = () => { + toast({ + title: "Buy Order Placed", + description: `Buying ${buyAmount} ${selectedPair.split("/")[0]} at $${buyPrice}`, + }) + setBuyAmount("") + setBuyPrice("") + setBuyPercentage([0]) + } + + const handleSell = () => { + toast({ + title: "Sell Order Placed", + description: `Selling ${sellAmount} ${selectedPair.split("/")[0]} at $${sellPrice}`, + }) + setSellAmount("") + setSellPrice("") + setSellPercentage([0]) + } + + return ( +
+
+
Trade
+
+
+ + + + Buy + + + Sell + + + + +
+ + setBuyPrice(e.target.value)} + className="bg-background text-foreground" + /> +
+ +
+ + setBuyAmount(e.target.value)} + className="bg-background text-foreground" + /> +
+ +
+
+ + {buyPercentage[0]}% +
+ +
+ 0% + 25% + 50% + 75% + 100% +
+
+ +
+
+ Available Balance: + 10,000.00 USD +
+
+ Total: + + {buyAmount && buyPrice + ? (Number.parseFloat(buyAmount) * Number.parseFloat(buyPrice)).toFixed(2) + : "0.00"}{" "} + USD + +
+
+ + +
+ + +
+ + setSellPrice(e.target.value)} + className="bg-background text-foreground" + /> +
+ +
+ + setSellAmount(e.target.value)} + className="bg-background text-foreground" + /> +
+ +
+
+ + {sellPercentage[0]}% +
+ +
+ 0% + 25% + 50% + 75% + 100% +
+
+ +
+
+ Available Balance: + 0.5234 {selectedPair.split("/")[0]} +
+
+ Total: + + {sellAmount && sellPrice + ? (Number.parseFloat(sellAmount) * Number.parseFloat(sellPrice)).toFixed(2) + : "0.00"}{" "} + USD + +
+
+ + +
+
+
+
+ ) +} diff --git a/frontend/components/trading-view-chart.tsx b/frontend/components/trading-view-chart.tsx new file mode 100644 index 0000000..d511679 --- /dev/null +++ b/frontend/components/trading-view-chart.tsx @@ -0,0 +1,55 @@ +"use client" + +import { useEffect, useRef, memo } from "react" + +interface TradingViewChartProps { + symbol: string +} + +export const TradingViewChart = memo(function TradingViewChart({ symbol }: TradingViewChartProps) { + const container = useRef(null) + + useEffect(() => { + if (!container.current) return + + // Clear previous widget + container.current.innerHTML = "" + + const script = document.createElement("script") + script.src = "https://s3.tradingview.com/external-embedding/embed-widget-advanced-chart.js" + script.type = "text/javascript" + script.async = true + script.innerHTML = JSON.stringify({ + autosize: true, + symbol: symbol.replace("/", ""), + interval: "D", + timezone: "Etc/UTC", + theme: "dark", + style: "1", + locale: "en", + enable_publishing: false, + backgroundColor: "rgba(22, 22, 22, 1)", + gridColor: "rgba(42, 42, 42, 1)", + hide_top_toolbar: false, + hide_legend: false, + save_image: false, + container_id: "tradingview_chart", + height: "600", + width: "100%", + }) + + container.current.appendChild(script) + + return () => { + if (container.current) { + container.current.innerHTML = "" + } + } + }, [symbol]) + + return ( +
+
+
+ ) +}) diff --git a/frontend/components/ui/accordion.tsx b/frontend/components/ui/accordion.tsx new file mode 100644 index 0000000..e538a33 --- /dev/null +++ b/frontend/components/ui/accordion.tsx @@ -0,0 +1,66 @@ +'use client' + +import * as React from 'react' +import * as AccordionPrimitive from '@radix-ui/react-accordion' +import { ChevronDownIcon } from 'lucide-react' + +import { cn } from '@/lib/utils' + +function Accordion({ + ...props +}: React.ComponentProps) { + return +} + +function AccordionItem({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AccordionTrigger({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + svg]:rotate-180', + className, + )} + {...props} + > + {children} + + + + ) +} + +function AccordionContent({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + +
{children}
+
+ ) +} + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/frontend/components/ui/alert-dialog.tsx b/frontend/components/ui/alert-dialog.tsx new file mode 100644 index 0000000..9704452 --- /dev/null +++ b/frontend/components/ui/alert-dialog.tsx @@ -0,0 +1,157 @@ +'use client' + +import * as React from 'react' +import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog' + +import { cn } from '@/lib/utils' +import { buttonVariants } from '@/components/ui/button' + +function AlertDialog({ + ...props +}: React.ComponentProps) { + return +} + +function AlertDialogTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogPortal({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + + ) +} + +function AlertDialogHeader({ + className, + ...props +}: React.ComponentProps<'div'>) { + return ( +
+ ) +} + +function AlertDialogFooter({ + className, + ...props +}: React.ComponentProps<'div'>) { + return ( +
+ ) +} + +function AlertDialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogAction({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogCancel({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +} diff --git a/frontend/components/ui/alert.tsx b/frontend/components/ui/alert.tsx new file mode 100644 index 0000000..e6751ab --- /dev/null +++ b/frontend/components/ui/alert.tsx @@ -0,0 +1,66 @@ +import * as React from 'react' +import { cva, type VariantProps } from 'class-variance-authority' + +import { cn } from '@/lib/utils' + +const alertVariants = cva( + 'relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current', + { + variants: { + variant: { + default: 'bg-card text-card-foreground', + destructive: + 'text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90', + }, + }, + defaultVariants: { + variant: 'default', + }, + }, +) + +function Alert({ + className, + variant, + ...props +}: React.ComponentProps<'div'> & VariantProps) { + return ( +
+ ) +} + +function AlertTitle({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ) +} + +function AlertDescription({ + className, + ...props +}: React.ComponentProps<'div'>) { + return ( +
+ ) +} + +export { Alert, AlertTitle, AlertDescription } diff --git a/frontend/components/ui/aspect-ratio.tsx b/frontend/components/ui/aspect-ratio.tsx new file mode 100644 index 0000000..40bb120 --- /dev/null +++ b/frontend/components/ui/aspect-ratio.tsx @@ -0,0 +1,11 @@ +'use client' + +import * as AspectRatioPrimitive from '@radix-ui/react-aspect-ratio' + +function AspectRatio({ + ...props +}: React.ComponentProps) { + return +} + +export { AspectRatio } diff --git a/frontend/components/ui/avatar.tsx b/frontend/components/ui/avatar.tsx new file mode 100644 index 0000000..aa98465 --- /dev/null +++ b/frontend/components/ui/avatar.tsx @@ -0,0 +1,53 @@ +'use client' + +import * as React from 'react' +import * as AvatarPrimitive from '@radix-ui/react-avatar' + +import { cn } from '@/lib/utils' + +function Avatar({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AvatarImage({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AvatarFallback({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/frontend/components/ui/badge.tsx b/frontend/components/ui/badge.tsx new file mode 100644 index 0000000..fc4126b --- /dev/null +++ b/frontend/components/ui/badge.tsx @@ -0,0 +1,46 @@ +import * as React from 'react' +import { Slot } from '@radix-ui/react-slot' +import { cva, type VariantProps } from 'class-variance-authority' + +import { cn } from '@/lib/utils' + +const badgeVariants = cva( + 'inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden', + { + variants: { + variant: { + default: + 'border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90', + secondary: + 'border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90', + destructive: + 'border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60', + outline: + 'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground', + }, + }, + defaultVariants: { + variant: 'default', + }, + }, +) + +function Badge({ + className, + variant, + asChild = false, + ...props +}: React.ComponentProps<'span'> & + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot : 'span' + + return ( + + ) +} + +export { Badge, badgeVariants } diff --git a/frontend/components/ui/breadcrumb.tsx b/frontend/components/ui/breadcrumb.tsx new file mode 100644 index 0000000..1750ff2 --- /dev/null +++ b/frontend/components/ui/breadcrumb.tsx @@ -0,0 +1,109 @@ +import * as React from 'react' +import { Slot } from '@radix-ui/react-slot' +import { ChevronRight, MoreHorizontal } from 'lucide-react' + +import { cn } from '@/lib/utils' + +function Breadcrumb({ ...props }: React.ComponentProps<'nav'>) { + return