From 13681e2a78e09e1eeb0925e95d35560e4a622120 Mon Sep 17 00:00:00 2001 From: crthpl Date: Sun, 8 Mar 2026 09:24:24 -0400 Subject: [PATCH 1/4] Allow sudoed admins higher decimal precision for orders and settlements Sudoed admins can now use: - 2 decimal places for settlement values (create market min/max, settle price) - 4 decimal places for order prices and sizes Regular users retain 1 decimal place (frontend) / 2 decimal places (backend). --- backend/src/db.rs | 27 +++++++++++++++++-- backend/src/handle_socket.rs | 2 +- backend/src/seed.rs | 2 +- .../lib/components/forms/createMarket.svelte | 14 ++++++---- .../lib/components/forms/settleMarket.svelte | 16 ++++++----- .../src/lib/components/marketDataUtils.ts | 14 ++++++++++ .../src/lib/components/marketOrders.svelte | 14 ++++++---- 7 files changed, 69 insertions(+), 20 deletions(-) diff --git a/backend/src/db.rs b/backend/src/db.rs index 64e52dd9..e1f93b28 100644 --- a/backend/src/db.rs +++ b/backend/src/db.rs @@ -2663,6 +2663,7 @@ impl DB { &self, owner_id: i64, create_order: websocket_api::CreateOrder, + is_sudo: bool, ) -> SqlxResult> { let Ok(price) = Decimal::try_from(create_order.price) else { return Ok(Err(ValidationFailure::InvalidPrice)); @@ -2681,11 +2682,12 @@ impl DB { let price = price.normalize(); let size = size.normalize(); - if price.scale() > 2 || price.mantissa() > 1_000_000_000_000 { + let max_scale = if is_sudo { 4 } else { 2 }; + if price.scale() > max_scale || price.mantissa() > 1_000_000_000_000 { return Ok(Err(ValidationFailure::InvalidPrice)); } - if size <= dec!(0) || size.scale() > 2 || size.mantissa() > 1_000_000_000_000 { + if size <= dec!(0) || size.scale() > max_scale || size.mantissa() > 1_000_000_000_000 { return Ok(Err(ValidationFailure::InvalidSize)); } @@ -4602,6 +4604,7 @@ mod tests { size: 1.0, side: websocket_api::Side::Bid as i32, }, + false, ) .await?; assert!(matches!(order_status, Err(ValidationFailure::InvalidPrice))); @@ -4615,6 +4618,7 @@ mod tests { size: 100.0, side: websocket_api::Side::Bid as i32, }, + false, ) .await?; assert!(matches!( @@ -4631,6 +4635,7 @@ mod tests { size: 100.0, side: websocket_api::Side::Offer as i32, }, + false, ) .await?; assert!(matches!( @@ -4647,6 +4652,7 @@ mod tests { size: -1.0, side: websocket_api::Side::Bid as i32, }, + false, ) .await?; assert!(matches!(order_status, Err(ValidationFailure::InvalidSize))); @@ -4660,6 +4666,7 @@ mod tests { size: 1.0, side: websocket_api::Side::Bid as i32, }, + false, ) .await?; assert!(matches!(order_status, Err(ValidationFailure::InvalidPrice))); @@ -4673,6 +4680,7 @@ mod tests { size: 1.0, side: websocket_api::Side::Offer as i32, }, + false, ) .await?; assert!(order_status.is_ok()); @@ -4696,6 +4704,7 @@ mod tests { size: 1.0, side: websocket_api::Side::Bid as i32, }, + false, ) .await?; let Ok(OrderCreated { @@ -4771,6 +4780,7 @@ mod tests { size: 1.0, side: websocket_api::Side::Offer as i32, }, + false, ) .await?; assert!(matches!( @@ -4811,6 +4821,7 @@ mod tests { size: 1.0, side: websocket_api::Side::Bid as i32, }, + false, ) .await? .unwrap(); @@ -4823,6 +4834,7 @@ mod tests { size: 1.0, side: websocket_api::Side::Offer as i32, }, + false, ) .await? .unwrap(); @@ -4853,6 +4865,7 @@ mod tests { size: 0.5, side: websocket_api::Side::Offer as i32, }, + false, ) .await?; @@ -4927,6 +4940,7 @@ mod tests { size: 1.0, side: websocket_api::Side::Bid as i32, }, + false, ) .await? .unwrap(); @@ -4940,6 +4954,7 @@ mod tests { size: 0.5, side: websocket_api::Side::Offer as i32, }, + false, ) .await?; @@ -4987,6 +5002,7 @@ mod tests { size: 10.0, side: websocket_api::Side::Bid as i32, }, + false, ) .await? .unwrap(); @@ -5000,6 +5016,7 @@ mod tests { size: 15.0, side: websocket_api::Side::Bid as i32, }, + false, ) .await?; @@ -5016,6 +5033,7 @@ mod tests { size: 10.0, side: websocket_api::Side::Bid as i32, }, + false, ) .await? .unwrap(); @@ -5063,6 +5081,7 @@ mod tests { size: 1.0, side: websocket_api::Side::Bid as i32, }, + false, ) .await? .unwrap(); @@ -5074,6 +5093,7 @@ mod tests { size: 1.0, side: websocket_api::Side::Bid as i32, }, + false, ) .await? .unwrap(); @@ -5085,6 +5105,7 @@ mod tests { size: 1.0, side: websocket_api::Side::Bid as i32, }, + false, ) .await? .unwrap(); @@ -5096,6 +5117,7 @@ mod tests { size: 1.0, side: websocket_api::Side::Bid as i32, }, + false, ) .await? .unwrap(); @@ -5109,6 +5131,7 @@ mod tests { size: 4.0, side: websocket_api::Side::Offer as i32, }, + false, ) .await?; diff --git a/backend/src/handle_socket.rs b/backend/src/handle_socket.rs index d4a9f224..c2f3f3b0 100644 --- a/backend/src/handle_socket.rs +++ b/backend/src/handle_socket.rs @@ -663,7 +663,7 @@ async fn handle_client_message( } CM::CreateOrder(create_order) => { check_mutate_rate_limit!("CreateOrder"); - match db.create_order(acting_as, create_order).await? { + match db.create_order(acting_as, create_order, admin_id.is_some()).await? { Ok(order_created) => { for user_id in order_created.fills.iter().map(|fill| &fill.owner_id) { subscriptions.notify_portfolio(*user_id); diff --git a/backend/src/seed.rs b/backend/src/seed.rs index 17cc7558..c99b1e72 100644 --- a/backend/src/seed.rs +++ b/backend/src/seed.rs @@ -289,7 +289,7 @@ pub async fn seed_dev_data(db: &DB, pool: &SqlitePool) -> Result<(), anyhow::Err side: order.side.into(), }; - match db.create_order(account_id, create_order).await? { + match db.create_order(account_id, create_order, false).await? { Ok(result) => { let trades_count = result.trades.len(); if trades_count > 0 { diff --git a/frontend/src/lib/components/forms/createMarket.svelte b/frontend/src/lib/components/forms/createMarket.svelte index cee9bc61..0d23a5d1 100644 --- a/frontend/src/lib/components/forms/createMarket.svelte +++ b/frontend/src/lib/components/forms/createMarket.svelte @@ -7,13 +7,17 @@ import { Input } from '$lib/components/ui/input'; import { Textarea } from '$lib/components/ui/textarea'; import { Checkbox } from '$lib/components/ui/checkbox'; - import { roundToTenth } from '$lib/components/marketDataUtils'; + import { roundToTenth, roundToHundredth } from '$lib/components/marketDataUtils'; import { websocket_api } from 'schema-js'; import { protoSuperForm } from './protoSuperForm'; import type { Snippet } from 'svelte'; import X from '@lucide/svelte/icons/x'; import Plus from '@lucide/svelte/icons/plus'; + const isSudo = $derived(serverState.isAdmin && serverState.sudoEnabled); + const settlementStep = $derived(isSudo ? '0.01' : '0.1'); + const roundSettlement = $derived(isSudo ? roundToHundredth : roundToTenth); + interface Props { children: Snippet; onclick?: () => void; @@ -221,10 +225,10 @@ {...props} type="number" max="1000000000000" - step="0.1" + step={settlementStep} bind:value={$formData.minSettlement} onblur={() => { - $formData.minSettlement = roundToTenth( + $formData.minSettlement = roundSettlement( $formData.minSettlement as unknown as number ); }} @@ -241,10 +245,10 @@ {...props} type="number" max="1000000000000" - step="0.1" + step={settlementStep} bind:value={$formData.maxSettlement} onblur={() => { - $formData.maxSettlement = roundToTenth( + $formData.maxSettlement = roundSettlement( $formData.maxSettlement as unknown as number ); }} diff --git a/frontend/src/lib/components/forms/settleMarket.svelte b/frontend/src/lib/components/forms/settleMarket.svelte index 974f798a..ef95ee84 100644 --- a/frontend/src/lib/components/forms/settleMarket.svelte +++ b/frontend/src/lib/components/forms/settleMarket.svelte @@ -1,18 +1,22 @@