diff --git a/coordinator/src/node/expired_positions.rs b/coordinator/src/node/expired_positions.rs index b34ae372a..82ef5aa58 100644 --- a/coordinator/src/node/expired_positions.rs +++ b/coordinator/src/node/expired_positions.rs @@ -11,10 +11,9 @@ use anyhow::Result; use commons::average_execution_price; use commons::Match; use commons::MatchState; -use commons::NewOrder; +use commons::NewMarketOrder; use commons::OrderReason; use commons::OrderState; -use commons::OrderType; use rust_decimal::prelude::FromPrimitive; use rust_decimal::prelude::ToPrimitive; use rust_decimal::Decimal; @@ -80,17 +79,13 @@ pub async fn close(node: Node, trading_sender: mpsc::Sender) -> tracing::debug!(trader_pk=%position.trader, %position.expiry_timestamp, "Attempting to close expired position"); - let new_order = NewOrder { + let new_order = NewMarketOrder { id: uuid::Uuid::new_v4(), contract_symbol: position.contract_symbol, - // TODO(holzeis): we should not have to set the price for a market order. we propably - // need separate models for a limit and a market order. - price: Decimal::ZERO, quantity: Decimal::try_from(position.quantity).expect("to fit into decimal"), trader_id: position.trader, direction: position.trader_direction.opposite(), leverage: Decimal::from_f32(position.trader_leverage).expect("to fit into decimal"), - order_type: OrderType::Market, // This order can basically not expire, but if the user does not come back online within // a certain time period we can assume the channel to be abandoned and we should force // close. @@ -98,7 +93,7 @@ pub async fn close(node: Node, trading_sender: mpsc::Sender) -> stable: position.stable, }; - let order = orders::insert(&mut conn, new_order.clone(), OrderReason::Expired) + let order = orders::insert_market_order(&mut conn, new_order.clone(), OrderReason::Expired) .map_err(|e| anyhow!(e)) .context("Failed to insert expired order into DB")?; diff --git a/coordinator/src/orderbook/db/orders.rs b/coordinator/src/orderbook/db/orders.rs index 355f122ae..8223bfd46 100644 --- a/coordinator/src/orderbook/db/orders.rs +++ b/coordinator/src/orderbook/db/orders.rs @@ -7,7 +7,8 @@ use crate::orderbook::db::custom_types::OrderType; use crate::schema::matches; use crate::schema::orders; use bitcoin::secp256k1::PublicKey; -use commons::NewOrder as OrderbookNewOrder; +use commons::NewLimitOrder; +use commons::NewMarketOrder; use commons::Order as OrderbookOrder; use commons::OrderReason as OrderBookOrderReason; use commons::OrderState as OrderBookOrderState; @@ -159,8 +160,8 @@ struct NewOrder { pub stable: bool, } -impl From for NewOrder { - fn from(value: OrderbookNewOrder) -> Self { +impl From for NewOrder { + fn from(value: NewLimitOrder) -> Self { NewOrder { trader_order_id: value.id, price: value @@ -175,7 +176,33 @@ impl From for NewOrder { .round_dp(2) .to_f32() .expect("To be able to convert decimal to f32"), - order_type: value.order_type.into(), + order_type: OrderType::Limit, + expiry: value.expiry, + order_reason: OrderReason::Manual, + contract_symbol: value.contract_symbol.into(), + leverage: value + .leverage + .to_f32() + .expect("To be able to convert decimal to f32"), + stable: value.stable, + } + } +} + +impl From for NewOrder { + fn from(value: NewMarketOrder) -> Self { + NewOrder { + trader_order_id: value.id, + // TODO: it would be cool to get rid of this as well + price: 0.0, + trader_id: value.trader_id.to_string(), + direction: value.direction.into(), + quantity: value + .quantity + .round_dp(2) + .to_f32() + .expect("To be able to convert decimal to f32"), + order_type: OrderType::Market, expiry: value.expiry, order_reason: OrderReason::Manual, contract_symbol: value.contract_symbol.into(), @@ -242,9 +269,26 @@ pub fn get_all_orders( } /// Returns the number of affected rows: 1. -pub fn insert( +pub fn insert_limit_order( + conn: &mut PgConnection, + order: NewLimitOrder, + order_reason: OrderBookOrderReason, +) -> QueryResult { + let new_order = NewOrder { + order_reason: OrderReason::from(order_reason), + ..NewOrder::from(order) + }; + let order: Order = diesel::insert_into(orders::table) + .values(new_order) + .get_result(conn)?; + + Ok(OrderbookOrder::from(order)) +} + +/// Returns the number of affected rows: 1. +pub fn insert_market_order( conn: &mut PgConnection, - order: OrderbookNewOrder, + order: NewMarketOrder, order_reason: OrderBookOrderReason, ) -> QueryResult { let new_order = NewOrder { diff --git a/coordinator/src/orderbook/routes.rs b/coordinator/src/orderbook/routes.rs index 524c87a98..61f126624 100644 --- a/coordinator/src/orderbook/routes.rs +++ b/coordinator/src/orderbook/routes.rs @@ -14,6 +14,7 @@ use axum::extract::State; use axum::response::IntoResponse; use axum::Json; use commons::Message; +use commons::NewOrder; use commons::NewOrderRequest; use commons::Order; use commons::OrderReason; @@ -76,7 +77,7 @@ pub async fn post_order( let new_order = new_order_request.value; // TODO(holzeis): We should add a similar check eventually for limit orders (makers). - if new_order.order_type == OrderType::Market { + if let NewOrder::Market(new_order) = &new_order { let mut conn = state .pool .get() @@ -87,7 +88,7 @@ pub async fn post_order( let settings = state.settings.read().await; - if OrderType::Limit == new_order.order_type { + if let NewOrder::Limit(new_order) = &new_order { if settings.whitelist_enabled && !settings.whitelisted_makers.contains(&new_order.trader_id) { tracing::warn!( @@ -105,12 +106,20 @@ pub async fn post_order( } let pool = state.pool.clone(); + let new_order = new_order.clone(); let order = spawn_blocking(move || { let mut conn = pool.get()?; - let order = orders::insert(&mut conn, new_order.clone(), OrderReason::Manual) - .map_err(|e| anyhow!(e)) - .context("Failed to insert new order into DB")?; + let order = match new_order { + NewOrder::Market(o) => { + orders::insert_market_order(&mut conn, o.clone(), OrderReason::Manual) + } + NewOrder::Limit(o) => { + orders::insert_limit_order(&mut conn, o.clone(), OrderReason::Manual) + } + } + .map_err(|e| anyhow!(e)) + .context("Failed to insert new order into DB")?; anyhow::Ok(order) }) diff --git a/coordinator/src/orderbook/tests/sample_test.rs b/coordinator/src/orderbook/tests/sample_test.rs index 0a719c683..575141673 100644 --- a/coordinator/src/orderbook/tests/sample_test.rs +++ b/coordinator/src/orderbook/tests/sample_test.rs @@ -3,10 +3,10 @@ use crate::orderbook::db::orders; use crate::orderbook::tests::setup_db; use crate::orderbook::tests::start_postgres; use bitcoin::secp256k1::PublicKey; -use commons::NewOrder; +use commons::NewLimitOrder; +use commons::NewMarketOrder; use commons::OrderReason; use commons::OrderState; -use commons::OrderType; use rust_decimal_macros::dec; use std::str::FromStr; use testcontainers::clients::Cli; @@ -24,12 +24,9 @@ async fn crud_test() { let mut conn = setup_db(conn_spec); - let order = orders::insert( + let order = orders::insert_limit_order( &mut conn, - dummy_order( - OffsetDateTime::now_utc() + Duration::minutes(1), - OrderType::Market, - ), + dummy_limit_order(OffsetDateTime::now_utc() + Duration::minutes(1)), OrderReason::Manual, ) .unwrap(); @@ -50,43 +47,39 @@ async fn test_all_limit_orders() { let orders = orders::all_limit_orders(&mut conn).unwrap(); assert!(orders.is_empty()); - orders::insert( - &mut conn, - dummy_order( - OffsetDateTime::now_utc() + Duration::minutes(1), - OrderType::Market, - ), - OrderReason::Manual, - ) - .unwrap(); + let order_1 = dummy_limit_order(OffsetDateTime::now_utc() + Duration::minutes(1)); + orders::insert_limit_order(&mut conn, order_1, OrderReason::Manual).unwrap(); - let second_order = orders::insert( - &mut conn, - dummy_order( - OffsetDateTime::now_utc() + Duration::minutes(1), - OrderType::Limit, - ), - OrderReason::Manual, - ) - .unwrap(); - orders::set_order_state(&mut conn, second_order.id, OrderState::Failed).unwrap(); + let order_2 = dummy_market_order(OffsetDateTime::now_utc() + Duration::minutes(1)); + orders::insert_market_order(&mut conn, order_2, OrderReason::Manual).unwrap(); - orders::insert( - &mut conn, - dummy_order( - OffsetDateTime::now_utc() + Duration::minutes(1), - OrderType::Limit, - ), - OrderReason::Manual, - ) - .unwrap(); + let order_3 = dummy_limit_order(OffsetDateTime::now_utc() + Duration::minutes(1)); + let second_limit_order = + orders::insert_limit_order(&mut conn, order_3, OrderReason::Manual).unwrap(); + orders::set_order_state(&mut conn, second_limit_order.id, OrderState::Failed).unwrap(); let orders = orders::all_limit_orders(&mut conn).unwrap(); assert_eq!(orders.len(), 1); } -fn dummy_order(expiry: OffsetDateTime, order_type: OrderType) -> NewOrder { - NewOrder { +fn dummy_market_order(expiry: OffsetDateTime) -> NewMarketOrder { + NewMarketOrder { + id: Uuid::new_v4(), + trader_id: PublicKey::from_str( + "027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007", + ) + .unwrap(), + direction: Direction::Long, + quantity: dec!(100.0), + expiry, + contract_symbol: trade::ContractSymbol::BtcUsd, + leverage: dec!(1.0), + stable: false, + } +} + +fn dummy_limit_order(expiry: OffsetDateTime) -> NewLimitOrder { + NewLimitOrder { id: Uuid::new_v4(), price: dec!(20000.00), trader_id: PublicKey::from_str( @@ -95,7 +88,6 @@ fn dummy_order(expiry: OffsetDateTime, order_type: OrderType) -> NewOrder { .unwrap(), direction: Direction::Long, quantity: dec!(100.0), - order_type, expiry, contract_symbol: trade::ContractSymbol::BtcUsd, leverage: dec!(1.0), diff --git a/crates/commons/src/order.rs b/crates/commons/src/order.rs index b111eaa1b..68d372279 100644 --- a/crates/commons/src/order.rs +++ b/crates/commons/src/order.rs @@ -24,7 +24,7 @@ pub struct NewOrderRequest { impl NewOrderRequest { pub fn verify(&self, secp: &secp256k1::Secp256k1) -> Result<()> { let message = self.value.message(); - let public_key = self.value.trader_id; + let public_key = self.value.trader_id(); secp.verify_ecdsa(&message, &self.signature, &public_key)?; Ok(()) @@ -32,7 +32,73 @@ impl NewOrderRequest { } #[derive(Serialize, Deserialize, Clone)] -pub struct NewOrder { +pub enum NewOrder { + Market(NewMarketOrder), + Limit(NewLimitOrder), +} + +impl NewOrder { + pub fn message(&self) -> Message { + match self { + NewOrder::Market(o) => o.message(), + NewOrder::Limit(o) => o.message(), + } + } + + pub fn trader_id(&self) -> PublicKey { + match self { + NewOrder::Market(o) => o.trader_id, + NewOrder::Limit(o) => o.trader_id, + } + } + + pub fn id(&self) -> Uuid { + match self { + NewOrder::Market(o) => o.id, + NewOrder::Limit(o) => o.id, + } + } + + pub fn direction(&self) -> Direction { + match self { + NewOrder::Market(o) => o.direction, + NewOrder::Limit(o) => o.direction, + } + } + + pub fn price(&self) -> String { + match self { + NewOrder::Market(_) => "Market".to_string(), + NewOrder::Limit(o) => o.price.to_string(), + } + } + + pub fn order_type(&self) -> String { + match self { + NewOrder::Market(_) => "Market", + NewOrder::Limit(_) => "Limit", + } + .to_string() + } +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct NewMarketOrder { + pub id: Uuid, + pub contract_symbol: ContractSymbol, + #[serde(with = "rust_decimal::serde::float")] + pub quantity: Decimal, + pub trader_id: PublicKey, + pub direction: Direction, + #[serde(with = "rust_decimal::serde::float")] + pub leverage: Decimal, + #[serde(with = "time::serde::timestamp")] + pub expiry: OffsetDateTime, + pub stable: bool, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct NewLimitOrder { pub id: Uuid, pub contract_symbol: ContractSymbol, #[serde(with = "rust_decimal::serde::float")] @@ -43,13 +109,12 @@ pub struct NewOrder { pub direction: Direction, #[serde(with = "rust_decimal::serde::float")] pub leverage: Decimal, - pub order_type: OrderType, #[serde(with = "time::serde::timestamp")] pub expiry: OffsetDateTime, pub stable: bool, } -impl NewOrder { +impl NewLimitOrder { pub fn message(&self) -> Message { let mut vec: Vec = vec![]; let mut id = self.id.as_bytes().to_vec(); @@ -58,8 +123,6 @@ impl NewOrder { let symbol = self.contract_symbol.label(); let symbol = symbol.as_bytes(); - let order_type = self.order_type.label(); - let order_type = order_type.as_bytes(); let direction = self.direction.to_string(); let direction = direction.as_bytes(); let quantity = format!("{:.2}", self.quantity); @@ -72,7 +135,6 @@ impl NewOrder { vec.append(&mut id); vec.append(&mut seconds); vec.append(&mut symbol.to_vec()); - vec.append(&mut order_type.to_vec()); vec.append(&mut direction.to_vec()); vec.append(&mut quantity.to_vec()); vec.append(&mut price.to_vec()); @@ -82,6 +144,33 @@ impl NewOrder { } } +impl NewMarketOrder { + pub fn message(&self) -> Message { + let mut vec: Vec = vec![]; + let mut id = self.id.as_bytes().to_vec(); + let unix_timestamp = self.expiry.unix_timestamp(); + let mut seconds = unix_timestamp.to_le_bytes().to_vec(); + + let symbol = self.contract_symbol.label(); + let symbol = symbol.as_bytes(); + let direction = self.direction.to_string(); + let direction = direction.as_bytes(); + let quantity = format!("{:.2}", self.quantity); + let quantity = quantity.as_bytes(); + let leverage = format!("{:.2}", self.leverage); + let leverage = leverage.as_bytes(); + + vec.append(&mut id); + vec.append(&mut seconds); + vec.append(&mut symbol.to_vec()); + vec.append(&mut direction.to_vec()); + vec.append(&mut quantity.to_vec()); + vec.append(&mut leverage.to_vec()); + + Message::from_hashed_data::(vec.as_slice()) + } +} + #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)] pub enum OrderType { #[allow(dead_code)] @@ -150,9 +239,9 @@ pub struct ChannelOpeningParams { #[cfg(test)] pub mod tests { + use crate::NewLimitOrder; use crate::NewOrder; use crate::NewOrderRequest; - use crate::OrderType; use secp256k1::rand; use secp256k1::Secp256k1; use secp256k1::SecretKey; @@ -169,7 +258,7 @@ pub mod tests { let secret_key = SecretKey::new(&mut rand::thread_rng()); let public_key = secret_key.public_key(SECP256K1); - let order = NewOrder { + let order = NewLimitOrder { id: Default::default(), contract_symbol: ContractSymbol::BtcUsd, price: rust_decimal_macros::dec!(53_000), @@ -177,7 +266,6 @@ pub mod tests { trader_id: public_key, direction: Direction::Long, leverage: rust_decimal_macros::dec!(2.0), - order_type: OrderType::Market, expiry: OffsetDateTime::now_utc(), stable: false, }; @@ -196,7 +284,7 @@ pub mod tests { .unwrap(); let public_key = secret_key.public_key(SECP256K1); - let original_order = NewOrder { + let original_order = NewLimitOrder { id: Uuid::from_str("67e5504410b1426f9247bb680e5fe0c8").unwrap(), contract_symbol: ContractSymbol::BtcUsd, price: rust_decimal_macros::dec!(53_000), @@ -204,7 +292,6 @@ pub mod tests { trader_id: public_key, direction: Direction::Long, leverage: rust_decimal_macros::dec!(2.0), - order_type: OrderType::Market, // Note: the last 5 is too much as it does not get serialized expiry: OffsetDateTime::UNIX_EPOCH + 1.1010101015.seconds(), stable: false, @@ -216,14 +303,14 @@ pub mod tests { signature.verify(&message, &public_key).unwrap(); let original_request = NewOrderRequest { - value: original_order, + value: NewOrder::Limit(original_order), signature, channel_opening_params: None, }; let original_serialized_request = serde_json::to_string(&original_request).unwrap(); - let serialized_msg = "{\"value\":{\"id\":\"67e55044-10b1-426f-9247-bb680e5fe0c8\",\"contract_symbol\":\"BtcUsd\",\"price\":53000.0,\"quantity\":2000.0,\"trader_id\":\"0218845781f631c48f1c9709e23092067d06837f30aa0cd0544ac887fe91ddd166\",\"direction\":\"Long\",\"leverage\":2.0,\"order_type\":\"Market\",\"expiry\":1,\"stable\":false},\"signature\":\"SIGNATURE_PLACEHOLDER\",\"channel_opening_params\":null}"; + let serialized_msg = "{\"value\":{\"Limit\":{\"id\":\"67e55044-10b1-426f-9247-bb680e5fe0c8\",\"contract_symbol\":\"BtcUsd\",\"price\":53000.0,\"quantity\":2000.0,\"trader_id\":\"0218845781f631c48f1c9709e23092067d06837f30aa0cd0544ac887fe91ddd166\",\"direction\":\"Long\",\"leverage\":2.0,\"expiry\":1,\"stable\":false}},\"signature\":\"304402205024fd6aea64c02155bdc063cf9168d9cd24fc6d54d3da0db645372828df210e022062323c30a88b60ef647d6740a01ac38fccc7f306f1c380bd92715d8b2e39adb9\",\"channel_opening_params\":null}"; // replace the signature with the one from above to have the same string let serialized_msg = diff --git a/crates/dev-maker/src/main.rs b/crates/dev-maker/src/main.rs index 980d2cf40..a4ed40a95 100644 --- a/crates/dev-maker/src/main.rs +++ b/crates/dev-maker/src/main.rs @@ -1,8 +1,8 @@ use crate::logger::init_tracing; use crate::orderbook_client::OrderbookClient; use anyhow::Result; +use commons::NewLimitOrder; use commons::NewOrder; -use commons::OrderType; use reqwest::Url; use rust_decimal::Decimal; use secp256k1::rand; @@ -99,7 +99,7 @@ async fn post_order( let uuid = Uuid::new_v4(); if let Err(err) = client .post_new_order( - NewOrder { + NewOrder::Limit(NewLimitOrder { id: uuid, contract_symbol: ContractSymbol::BtcUsd, price, @@ -107,11 +107,10 @@ async fn post_order( trader_id: public_key, direction, leverage: Decimal::from(2), - order_type: OrderType::Limit, expiry: OffsetDateTime::now_utc() + time::Duration::seconds(order_expiry_seconds as i64), stable: false, - }, + }), None, secret_key, ) diff --git a/crates/dev-maker/src/orderbook_client.rs b/crates/dev-maker/src/orderbook_client.rs index 4d82f4d9c..c20bc8d23 100644 --- a/crates/dev-maker/src/orderbook_client.rs +++ b/crates/dev-maker/src/orderbook_client.rs @@ -31,9 +31,9 @@ impl OrderbookClient { let url = self.url.join("/api/orderbook/orders")?; tracing::info!( - id = order.id.to_string(), - direction = order.direction.to_string(), - price = order.price.to_string(), + id = order.id().to_string(), + direction = order.direction().to_string(), + price = order.price().to_string(), "Posting order" ); let message = order.message(); diff --git a/mobile/lib/common/amount_input_modalised.dart b/mobile/lib/common/amount_input_modalised.dart index 524b77e62..c0957aa6b 100644 --- a/mobile/lib/common/amount_input_modalised.dart +++ b/mobile/lib/common/amount_input_modalised.dart @@ -110,7 +110,7 @@ class _EnterAmountModalState extends State { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ AmountInputField( - value: widget.amount != null ? Amount(widget.amount!) : Amount.zero(), + initialValue: widget.amount != null ? Amount(widget.amount!) : Amount.zero(), hint: "e.g. $hint", label: "Amount", validator: widget.validator, diff --git a/mobile/lib/common/amount_text_input_form_field.dart b/mobile/lib/common/amount_text_input_form_field.dart index 286cd9ad0..3f4d5b136 100644 --- a/mobile/lib/common/amount_text_input_form_field.dart +++ b/mobile/lib/common/amount_text_input_form_field.dart @@ -12,7 +12,7 @@ class AmountInputField extends StatelessWidget { this.label = '', this.hint = '', this.onChanged, - required this.value, + required this.initialValue, this.isLoading = false, this.suffixIcon, this.controller, @@ -23,7 +23,7 @@ class AmountInputField extends StatelessWidget { final TextEditingController? controller; final TextStyle? style; - final Amount value; + final Amount initialValue; final bool enabled; final String label; final String hint; @@ -41,7 +41,7 @@ class AmountInputField extends StatelessWidget { style: style ?? const TextStyle(color: Colors.black87, fontSize: 16), enabled: enabled, controller: controller, - initialValue: controller != null ? null : value.formatted(), + initialValue: controller != null ? null : initialValue.formatted(), keyboardType: TextInputType.number, decoration: decoration ?? InputDecoration( diff --git a/mobile/lib/common/domain/model.dart b/mobile/lib/common/domain/model.dart index 236403988..e83d5be4b 100644 --- a/mobile/lib/common/domain/model.dart +++ b/mobile/lib/common/domain/model.dart @@ -108,7 +108,7 @@ class Usd { String formatted() { final formatter = NumberFormat("#,###,###,###,###", "en"); - return formatter.format(_usd); + return formatter.format(_usd.toDouble()); } @override diff --git a/mobile/lib/features/trade/application/order_service.dart b/mobile/lib/features/trade/application/order_service.dart index 2d887212b..90ee90570 100644 --- a/mobile/lib/features/trade/application/order_service.dart +++ b/mobile/lib/features/trade/application/order_service.dart @@ -6,8 +6,8 @@ import 'package:get_10101/features/trade/domain/order.dart'; import 'package:get_10101/ffi.dart' as rust; class OrderService { - Future submitMarketOrder(Leverage leverage, Amount quantity, - ContractSymbol contractSymbol, Direction direction, bool stable) async { + Future submitMarketOrder(Leverage leverage, Usd quantity, ContractSymbol contractSymbol, + Direction direction, bool stable) async { rust.NewOrder order = rust.NewOrder( leverage: leverage.leverage, quantity: quantity.asDouble(), @@ -21,7 +21,7 @@ class OrderService { Future submitChannelOpeningMarketOrder( Leverage leverage, - Amount quantity, + Usd quantity, ContractSymbol contractSymbol, Direction direction, bool stable, diff --git a/mobile/lib/features/trade/application/trade_values_service.dart b/mobile/lib/features/trade/application/trade_values_service.dart index 995276cee..5e98e7f24 100644 --- a/mobile/lib/features/trade/application/trade_values_service.dart +++ b/mobile/lib/features/trade/application/trade_values_service.dart @@ -5,10 +5,7 @@ import 'package:get_10101/ffi.dart' as rust; class TradeValuesService { Amount? calculateMargin( - {required double? price, - required Amount? quantity, - required Leverage leverage, - dynamic hint}) { + {required double? price, required Usd? quantity, required Leverage leverage, dynamic hint}) { if (price == null || quantity == null) { return null; } else { @@ -17,14 +14,14 @@ class TradeValuesService { } } - Amount? calculateQuantity( + Usd? calculateQuantity( {required double? price, required Amount? margin, required Leverage leverage, dynamic hint}) { if (price == null || margin == null) { return null; } else { final quantity = rust.api .calculateQuantity(price: price, margin: margin.sats, leverage: leverage.leverage); - return Amount(quantity.ceil()); + return Usd(quantity.ceil()); } } @@ -41,7 +38,7 @@ class TradeValuesService { } } - Amount? orderMatchingFee({required Amount? quantity, required double? price}) { + Amount? orderMatchingFee({required Usd? quantity, required double? price}) { return quantity != null && price != null ? Amount(rust.api.orderMatchingFee(quantity: quantity.asDouble(), price: price)) : null; diff --git a/mobile/lib/features/trade/channel_configuration.dart b/mobile/lib/features/trade/channel_configuration.dart index 901364fb2..c9f189d7c 100644 --- a/mobile/lib/features/trade/channel_configuration.dart +++ b/mobile/lib/features/trade/channel_configuration.dart @@ -193,7 +193,7 @@ class _ChannelConfiguration extends State { children: [ Flexible( child: AmountInputField( - value: ownTotalCollateral, + initialValue: ownTotalCollateral, controller: _collateralController, label: 'Your collateral (sats)', onChanged: (value) { diff --git a/mobile/lib/features/trade/domain/position.dart b/mobile/lib/features/trade/domain/position.dart index 186871f75..09277fb34 100644 --- a/mobile/lib/features/trade/domain/position.dart +++ b/mobile/lib/features/trade/domain/position.dart @@ -26,7 +26,7 @@ enum PositionState { class Position { final Leverage leverage; - final Amount quantity; + final Usd quantity; final ContractSymbol contractSymbol; final Direction direction; final double averageEntryPrice; @@ -65,7 +65,7 @@ class Position { static Position fromApi(bridge.Position position) { return Position( leverage: Leverage(position.leverage), - quantity: Amount(position.quantity.ceil()), + quantity: Usd(position.quantity.ceil()), contractSymbol: ContractSymbol.fromApi(position.contractSymbol), direction: Direction.fromApi(position.direction), positionState: PositionState.fromApi(position.positionState), diff --git a/mobile/lib/features/trade/domain/trade_values.dart b/mobile/lib/features/trade/domain/trade_values.dart index 31bccd5aa..d7c0d4da8 100644 --- a/mobile/lib/features/trade/domain/trade_values.dart +++ b/mobile/lib/features/trade/domain/trade_values.dart @@ -9,7 +9,7 @@ class TradeValues { Direction direction; // These values can be null if coordinator is down - Amount? quantity; + Usd? quantity; double? price; double? liquidationPrice; Amount? fee; // This fee is an estimate of the order-matching fee. @@ -33,7 +33,7 @@ class TradeValues { required this.tradeValuesService}); factory TradeValues.fromQuantity( - {required Amount quantity, + {required Usd quantity, required Leverage leverage, required double? price, required double fundingRate, @@ -70,7 +70,7 @@ class TradeValues { required double fundingRate, required Direction direction, required TradeValuesService tradeValuesService}) { - Amount? quantity = + Usd? quantity = tradeValuesService.calculateQuantity(price: price, margin: margin, leverage: leverage); double? liquidationPrice = price != null ? tradeValuesService.calculateLiquidationPrice( @@ -94,7 +94,7 @@ class TradeValues { tradeValuesService: tradeValuesService); } - updateQuantity(Amount quantity) { + updateQuantity(Usd quantity) { this.quantity = quantity; _recalculateMargin(); _recalculateFee(); @@ -139,7 +139,7 @@ class TradeValues { } _recalculateQuantity() { - Amount? quantity = + Usd? quantity = tradeValuesService.calculateQuantity(price: price, margin: margin, leverage: leverage); this.quantity = quantity; } diff --git a/mobile/lib/features/trade/trade_bottom_sheet_tab.dart b/mobile/lib/features/trade/trade_bottom_sheet_tab.dart index 68aa7a102..f3e251583 100644 --- a/mobile/lib/features/trade/trade_bottom_sheet_tab.dart +++ b/mobile/lib/features/trade/trade_bottom_sheet_tab.dart @@ -49,6 +49,9 @@ class _TradeBottomSheetTabState extends State { bool showCapacityInfo = false; + bool marginInputFieldEnabled = false; + bool quantityInputFieldEnabled = true; + @override void initState() { provider = context.read(); @@ -224,21 +227,19 @@ class _TradeBottomSheetTabState extends State { children: [ Flexible( child: AmountInputField( - value: tradeValues.quantity ?? Amount.zero(), + initialValue: Amount(tradeValues.quantity?.toInt ?? 0), hint: "e.g. 100 USD", label: "Quantity (USD)", onChanged: (value) { - Amount quantity = Amount.zero(); + Usd quantity = Usd.zero(); try { if (value.isNotEmpty) { - quantity = Amount.parseAmount(value); + quantity = Usd.parseString(value); } context.read().updateQuantity(direction, quantity); } on Exception { - context - .read() - .updateQuantity(direction, Amount.zero()); + context.read().updateQuantity(direction, Usd.zero()); } _formKey.currentState?.validate(); }, diff --git a/mobile/lib/features/trade/trade_value_change_notifier.dart b/mobile/lib/features/trade/trade_value_change_notifier.dart index 5c21a8da3..b7afc9444 100644 --- a/mobile/lib/features/trade/trade_value_change_notifier.dart +++ b/mobile/lib/features/trade/trade_value_change_notifier.dart @@ -24,7 +24,7 @@ class TradeValuesChangeNotifier extends ChangeNotifier implements Subscriber { } TradeValues _initOrder(Direction direction) { - Amount defaultQuantity = Amount(500); + Usd defaultQuantity = Usd(500); Leverage defaultLeverage = Leverage(2); switch (direction) { @@ -71,7 +71,7 @@ class TradeValuesChangeNotifier extends ChangeNotifier implements Subscriber { return fromDirection(direction).fee; } - void updateQuantity(Direction direction, Amount quantity) { + void updateQuantity(Direction direction, Usd quantity) { fromDirection(direction).updateQuantity(quantity); notifyListeners(); } diff --git a/mobile/lib/features/wallet/receive/receive_usdp_dialog.dart b/mobile/lib/features/wallet/receive/receive_usdp_dialog.dart index 377f2e5ef..5e30dce2b 100644 --- a/mobile/lib/features/wallet/receive/receive_usdp_dialog.dart +++ b/mobile/lib/features/wallet/receive/receive_usdp_dialog.dart @@ -66,7 +66,7 @@ Widget createSubmitWidget( bottomText = "Sorry, we couldn't match your order. Please try again later."; break; case PendingOrderState.orderFilled: - var amount = pendingOrder.tradeValues?.quantity?.sats ?? "0"; + var amount = pendingOrder.tradeValues?.quantity?.toInt ?? "0"; bottomText = "Congratulations! You received $amount USDP."; break; } @@ -80,9 +80,7 @@ Widget createSubmitWidget( runSpacing: 10, children: [ ValueDataRow( - type: ValueType.fiat, - value: pendingOrderValues?.quantity?.sats.toDouble(), - label: "USDP"), + type: ValueType.fiat, value: pendingOrderValues?.quantity?.toInt, label: "USDP"), ValueDataRow( type: ValueType.amount, value: pendingOrderValues?.margin, label: "Margin"), ValueDataRow( diff --git a/mobile/lib/features/wallet/send/confirm_payment_modal.dart b/mobile/lib/features/wallet/send/confirm_payment_modal.dart index c6342fca4..a9166afac 100644 --- a/mobile/lib/features/wallet/send/confirm_payment_modal.dart +++ b/mobile/lib/features/wallet/send/confirm_payment_modal.dart @@ -17,7 +17,7 @@ import 'package:provider/provider.dart'; import 'package:slide_to_confirm/slide_to_confirm.dart'; void showConfirmPaymentModal( - BuildContext context, Destination destination, bool payWithUsdp, Amount sats, Amount usdp, + BuildContext context, Destination destination, bool payWithUsdp, Amount sats, Usd usdp, {Fee? fee}) { logger.i(fee); showModalBottomSheet( @@ -45,7 +45,7 @@ class ConfirmPayment extends StatelessWidget { final Destination destination; final bool payWithUsdp; final Amount sats; - final Amount usdp; + final Usd usdp; final Fee? fee; const ConfirmPayment( diff --git a/mobile/lib/features/wallet/send/fee_picker.dart b/mobile/lib/features/wallet/send/fee_picker.dart index 9a0f6bcdc..1fa3c67cf 100644 --- a/mobile/lib/features/wallet/send/fee_picker.dart +++ b/mobile/lib/features/wallet/send/fee_picker.dart @@ -198,7 +198,7 @@ class _FeePickerModalState extends State<_FeePickerModal> { "sats/vbyte", style: TextStyle(fontSize: 16, color: Color(0xff878787)), )), - value: Amount(1)), + initialValue: Amount(1)), ), ), const SizedBox(height: 25), diff --git a/mobile/lib/features/wallet/send/send_onchain_screen.dart b/mobile/lib/features/wallet/send/send_onchain_screen.dart index 4e337b585..d0cabcb96 100644 --- a/mobile/lib/features/wallet/send/send_onchain_screen.dart +++ b/mobile/lib/features/wallet/send/send_onchain_screen.dart @@ -320,8 +320,15 @@ class _SendOnChainScreenState extends State { width: MediaQuery.of(context).size.width * 0.9, child: ElevatedButton( onPressed: (_formKey.currentState?.validate() ?? false) - ? () => showConfirmPaymentModal(context, widget.destination, false, - _amount ?? Amount.zero(), _amount ?? Amount.zero(), fee: _fee) + ? () => showConfirmPaymentModal( + context, + widget.destination, + false, + _amount ?? Amount.zero(), + // this value doesn't matter at the moment because we do not support USDP sending + // TODO: remove USDP leftovers + Usd.zero(), + fee: _fee) : null, style: ButtonStyle( padding: diff --git a/mobile/native/src/trade/order/handler.rs b/mobile/native/src/trade/order/handler.rs index 6b617f02a..7a35093ad 100644 --- a/mobile/native/src/trade/order/handler.rs +++ b/mobile/native/src/trade/order/handler.rs @@ -92,7 +92,7 @@ pub async fn submit_order( let orderbook_client = OrderbookClient::new(url); if let Err(err) = orderbook_client - .post_new_order(order.clone().into(), channel_opening_params) + .post_new_market_order(order.clone().into(), channel_opening_params) .await { let order_id = order.id.clone().to_string(); diff --git a/mobile/native/src/trade/order/mod.rs b/mobile/native/src/trade/order/mod.rs index 79516c43c..c8c27bb96 100644 --- a/mobile/native/src/trade/order/mod.rs +++ b/mobile/native/src/trade/order/mod.rs @@ -193,21 +193,17 @@ impl Order { } } -impl From for commons::NewOrder { +impl From for commons::NewMarketOrder { fn from(order: Order) -> Self { let quantity = Decimal::try_from(order.quantity).expect("to parse into decimal"); let trader_id = ln_dlc::get_node_pubkey(); - commons::NewOrder { + commons::NewMarketOrder { id: order.id, contract_symbol: order.contract_symbol, - // todo: this is left out intentionally as market orders do not set a price. this field - // should either be an option or differently modelled for a market order. - price: Decimal::ZERO, quantity, trader_id, direction: order.direction, leverage: Decimal::from_f32(order.leverage).expect("to fit into f32"), - order_type: order.order_type.into(), expiry: order.order_expiry_timestamp, stable: order.stable, } diff --git a/mobile/native/src/trade/order/orderbook_client.rs b/mobile/native/src/trade/order/orderbook_client.rs index abce6ad96..47463dd44 100644 --- a/mobile/native/src/trade/order/orderbook_client.rs +++ b/mobile/native/src/trade/order/orderbook_client.rs @@ -3,6 +3,7 @@ use crate::ln_dlc::get_node_key; use anyhow::bail; use anyhow::Result; use commons::ChannelOpeningParams; +use commons::NewMarketOrder; use commons::NewOrder; use commons::NewOrderRequest; use reqwest::Url; @@ -16,16 +17,16 @@ impl OrderbookClient { Self { url } } - pub(crate) async fn post_new_order( + pub(crate) async fn post_new_market_order( &self, - order: NewOrder, + order: NewMarketOrder, channel_opening_params: Option, ) -> Result<()> { let secret_key = get_node_key(); let message = order.message(); let signature = secret_key.sign_ecdsa(message); let new_order_request = NewOrderRequest { - value: order, + value: NewOrder::Market(order), signature, channel_opening_params, }; diff --git a/mobile/test/trade_test.dart b/mobile/test/trade_test.dart index 7710f9d9a..2b6d1326b 100644 --- a/mobile/test/trade_test.dart +++ b/mobile/test/trade_test.dart @@ -102,7 +102,7 @@ void main() { .thenReturn(10000); when(tradeValueService.calculateQuantity( price: anyNamed('price'), leverage: anyNamed('leverage'), margin: anyNamed('margin'))) - .thenReturn(Amount(1)); + .thenReturn(Usd(1)); when(tradeValueService.getExpiryTimestamp()).thenReturn(DateTime.now()); when(tradeValueService.orderMatchingFee( quantity: anyNamed('quantity'), price: anyNamed('price'))) @@ -217,7 +217,7 @@ void main() { .thenReturn(10000); when(tradeValueService.calculateQuantity( price: anyNamed('price'), leverage: anyNamed('leverage'), margin: anyNamed('margin'))) - .thenReturn(Amount(1)); + .thenReturn(Usd(1)); when(tradeValueService.getExpiryTimestamp()).thenReturn(DateTime.now()); when(tradeValueService.orderMatchingFee( quantity: anyNamed('quantity'), price: anyNamed('price'))) diff --git a/webapp/frontend/lib/common/amount_text_input_form_field.dart b/webapp/frontend/lib/common/amount_text_input_form_field.dart index 5bade5e71..f0eb05121 100644 --- a/webapp/frontend/lib/common/amount_text_input_form_field.dart +++ b/webapp/frontend/lib/common/amount_text_input_form_field.dart @@ -13,7 +13,7 @@ class AmountInputField extends StatelessWidget { this.label = '', this.hint = '', this.onChanged, - this.value, + this.initialValue, this.isLoading = false, this.infoText, this.controller, @@ -27,7 +27,7 @@ class AmountInputField extends StatelessWidget { final TextEditingController? controller; final TextStyle? style; - final Formattable? value; + final Formattable? initialValue; final bool enabled; final String label; final String hint; @@ -48,7 +48,7 @@ class AmountInputField extends StatelessWidget { enabled: enabled, controller: controller, textAlign: textAlign, - initialValue: controller != null ? null : value?.formatted(), + initialValue: controller != null ? null : initialValue?.formatted(), keyboardType: TextInputType.number, decoration: decoration ?? InputDecoration( diff --git a/webapp/frontend/lib/trade/trade_screen_order_form.dart b/webapp/frontend/lib/trade/trade_screen_order_form.dart index a9bb06d3a..0c404ab42 100644 --- a/webapp/frontend/lib/trade/trade_screen_order_form.dart +++ b/webapp/frontend/lib/trade/trade_screen_order_form.dart @@ -57,7 +57,7 @@ class _NewOrderForm extends State { Align( alignment: AlignmentDirectional.centerEnd, child: AmountInputField( - value: _quantity, + initialValue: _quantity, enabled: true, label: "Quantity", textAlign: TextAlign.right, diff --git a/webapp/frontend/lib/wallet/send_screen.dart b/webapp/frontend/lib/wallet/send_screen.dart index 6960b279b..4db3f56fe 100644 --- a/webapp/frontend/lib/wallet/send_screen.dart +++ b/webapp/frontend/lib/wallet/send_screen.dart @@ -73,7 +73,7 @@ class _SendScreenState extends State { ), const SizedBox(height: 20), AmountInputField( - value: amount != null ? amount! : Amount.zero(), + initialValue: amount != null ? amount! : Amount.zero(), label: "Amount in sats", controller: _amountController, validator: (value) { @@ -88,7 +88,7 @@ class _SendScreenState extends State { ), const SizedBox(height: 20), AmountInputField( - value: fee != null ? fee! : Amount.zero(), + initialValue: fee != null ? fee! : Amount.zero(), label: "Sats/vb", controller: _feeController, validator: (value) {