Skip to content

Commit

Permalink
feat(webapp): add order confirmation dialog
Browse files Browse the repository at this point in the history
Signed-off-by: Philipp Hoenisch <[email protected]>
  • Loading branch information
bonomat committed Mar 6, 2024
1 parent 4adb5f8 commit b377029
Show file tree
Hide file tree
Showing 6 changed files with 197 additions and 22 deletions.
1 change: 1 addition & 0 deletions crates/commons/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ pub use liquidity_option::*;
pub use message::*;
pub use order::*;
pub use order_matching_fee::order_matching_fee_taker;
pub use order_matching_fee::taker_fee;
pub use polls::*;
pub use price::best_current_price;
pub use price::Price;
Expand Down
4 changes: 4 additions & 0 deletions crates/commons/src/order_matching_fee.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ pub fn order_matching_fee_taker(quantity: f32, price: Decimal) -> bitcoin::Amoun
order_matching_fee(quantity, price, Decimal::new(TAKER_FEE.0, TAKER_FEE.1))
}

pub fn taker_fee() -> Decimal {
Decimal::new(TAKER_FEE.0, TAKER_FEE.1)
}

fn order_matching_fee(quantity: f32, price: Decimal, fee_per_cent: Decimal) -> bitcoin::Amount {
let quantity = Decimal::from_f32(quantity).expect("quantity to fit in Decimal");

Expand Down
146 changes: 146 additions & 0 deletions webapp/frontend/lib/trade/create_order_confirmation_dialog.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import 'package:flutter/material.dart';
import 'package:get_10101/common/contract_symbol_icon.dart';
import 'package:get_10101/common/direction.dart';
import 'package:get_10101/common/model.dart';
import 'package:get_10101/common/snack_bar.dart';
import 'package:get_10101/common/theme.dart';
import 'package:get_10101/common/value_data_row.dart';
import 'package:get_10101/trade/new_order_service.dart';
import 'package:get_10101/trade/quote_service.dart';

class CreateOrderConfirmationDialog extends StatelessWidget {
final Direction direction;
final Function() onConfirmation;
final Function() onCancel;
final BestQuote? bestQuote;
final Amount? fee;
final Leverage leverage;
final Usd quantity;

const CreateOrderConfirmationDialog(
{super.key,
required this.direction,
required this.onConfirmation,
required this.onCancel,
required this.bestQuote,
required this.fee,
required this.leverage,
required this.quantity});

@override
Widget build(BuildContext context) {
final messenger = ScaffoldMessenger.of(context);
TenTenOneTheme tradeTheme = Theme.of(context).extension<TenTenOneTheme>()!;

Price? price = bestQuote?.bid;
if (direction == Direction.short) {
price = bestQuote?.ask;
}

Color color = direction == Direction.long ? tradeTheme.buy : tradeTheme.sell;

return Dialog(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: SizedBox(
width: 340,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(20),
child: Column(
children: [
const ContractSymbolIcon(),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text("Market ${direction.nameU}",
style:
TextStyle(fontWeight: FontWeight.bold, fontSize: 17, color: color)),
),
Center(
child: Container(
padding: const EdgeInsets.symmetric(vertical: 10),
child: Column(
children: [
Wrap(
runSpacing: 10,
children: [
ValueDataRow(
type: ValueType.contracts,
value: quantity.formatted(),
label: 'Quantity'),
//
ValueDataRow(
type: ValueType.text,
value: leverage.formatted(),
label: 'Leverage'),
//
ValueDataRow(
type: ValueType.fiat,
value: price?.asDouble ?? 0.0,
label: 'Latest Market Price'),
//
ValueDataRow(
type: ValueType.amount,
value: fee ?? Amount.zero(),
label: "Fee estimate",
),
],
),
],
),
),
),
Padding(
padding: const EdgeInsets.only(top: 20.0),
child: RichText(
textAlign: TextAlign.justify,
text: TextSpan(
text:
'By confirming, a market order will be created. Once the order is matched your position will be updated.',
style: DefaultTextStyle.of(context).style)),
),
Padding(
padding: const EdgeInsets.only(top: 20.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
onPressed: () {
onCancel();
Navigator.pop(context);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.grey, fixedSize: const Size(100, 20)),
child: const Text('Cancel'),
),
ElevatedButton(
onPressed: () async {
await NewOrderService.postNewOrder(
leverage, quantity, direction == Direction.long.opposite())
.then((orderId) {
showSnackBar(
messenger, "Market order created. Order id: $orderId.");
Navigator.pop(context);
}).catchError((error) {
showSnackBar(messenger, "Failed creating market order: $error.");
}).whenComplete(onConfirmation);
},
style: ElevatedButton.styleFrom(fixedSize: const Size(100, 20)),
child: const Text('Accept'),
),
],
),
),
],
))
],
),
),
),
);
}
}
4 changes: 3 additions & 1 deletion webapp/frontend/lib/trade/quote_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ import 'dart:convert';
class BestQuote {
Price? bid;
Price? ask;
double? fee;

BestQuote({this.bid, this.ask});
BestQuote({this.bid, this.ask, this.fee});

factory BestQuote.fromJson(Map<String, dynamic> json) {
return BestQuote(
bid: (Price.parseString(json['bid'])),
ask: (Price.parseString(json['ask'])),
fee: json['fee'],
);
}
}
Expand Down
48 changes: 29 additions & 19 deletions webapp/frontend/lib/trade/trade_screen_order_form.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import 'package:bitcoin_icons/bitcoin_icons.dart';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get_10101/common/amount_text_input_form_field.dart';
import 'package:get_10101/common/direction.dart';
import 'package:get_10101/common/model.dart';
import 'package:get_10101/common/snack_bar.dart';
import 'package:get_10101/common/theme.dart';
import 'package:get_10101/trade/new_order_service.dart';
import 'package:get_10101/trade/create_order_confirmation_dialog.dart';
import 'package:get_10101/trade/quote_change_notifier.dart';
import 'package:get_10101/trade/quote_service.dart';
import 'package:provider/provider.dart';
Expand All @@ -27,7 +27,6 @@ class _NewOrderForm extends State<NewOrderForm> {
Usd? _quantity = Usd(100);
Leverage _leverage = Leverage(1);
bool isBuy = true;
bool _isLoading = false;

final TextEditingController _marginController = TextEditingController();
final TextEditingController _liquidationPriceController = TextEditingController();
Expand All @@ -44,6 +43,7 @@ class _NewOrderForm extends State<NewOrderForm> {
TenTenOneTheme theme = Theme.of(context).extension<TenTenOneTheme>()!;
Color buyButtonColor = isBuy ? theme.buy : theme.inactiveButtonColor;
Color sellButtonColor = isBuy ? theme.inactiveButtonColor : theme.sell;
final direction = isBuy ? Direction.long : Direction.short;

_quote = context.watch<QuoteChangeNotifier>().getBestQuote();

Expand Down Expand Up @@ -119,25 +119,26 @@ class _NewOrderForm extends State<NewOrderForm> {
Align(
alignment: AlignmentDirectional.center,
child: ElevatedButton(
onPressed: _isLoading
? null
: () {
final messenger = ScaffoldMessenger.of(context);
setState(() => _isLoading = true);
NewOrderService.postNewOrder(_leverage, _quantity!, isBuy).then((orderId) {
showSnackBar(messenger, "Order created $orderId.");
setState(() => _isLoading = false);
}).catchError((error) {
showSnackBar(messenger, "Posting a new order failed $error");
setState(() => _isLoading = false);
});
},
onPressed: () {
Amount? fee = calculateFee(_quantity, _quote, isBuy);
showDialog(
context: context,
builder: (BuildContext context) {
return CreateOrderConfirmationDialog(
direction: direction,
onConfirmation: () {},
onCancel: () {},
bestQuote: _quote,
fee: fee,
leverage: _leverage,
quantity: _quantity ?? Usd.zero(),
);
});
},
style: ElevatedButton.styleFrom(
backgroundColor: isBuy ? buyButtonColor : sellButtonColor,
minimumSize: const Size.fromHeight(50)),
child: _isLoading
? const CircularProgressIndicator()
: (isBuy ? const Text("Buy") : const Text("Sell"))),
child: (isBuy ? const Text("Buy") : const Text("Sell"))),
),
],
);
Expand All @@ -159,6 +160,15 @@ class _NewOrderForm extends State<NewOrderForm> {
}
}

Amount calculateFee(Usd? quantity, BestQuote? quote, bool isLong) {
if (quote?.fee == null || quote?.fee == 0 || quantity == null) {
return Amount.zero();
}

return Amount(
(calculateMargin(quantity, quote!, Leverage.one(), isLong).sats * quote.fee!).toInt());
}

Amount calculateMargin(Usd quantity, BestQuote quote, Leverage leverage, bool isLong) {
if (isLong && quote.ask != null) {
if (quote.ask!.asDouble == 0) {
Expand Down
16 changes: 14 additions & 2 deletions webapp/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use axum::Json;
use axum::Router;
use bitcoin::Amount;
use commons::order_matching_fee_taker;
use commons::taker_fee;
use commons::Price;
use dlc_manager::channel::signed_channel::SignedChannelState;
use native::api::ContractSymbol;
Expand Down Expand Up @@ -468,16 +469,27 @@ pub async fn get_orders() -> Result<Json<Vec<Order>>, AppError> {
Ok(Json(orders))
}

#[derive(Serialize)]
pub struct BestQuote {
#[serde(flatten)]
price: Price,
#[serde(with = "rust_decimal::serde::float")]
fee: Decimal,
}

pub async fn get_best_quote(
State(subscribers): State<Arc<AppSubscribers>>,
Path(contract_symbol): Path<ContractSymbol>,
) -> Result<Json<Option<Price>>, AppError> {
) -> Result<Json<Option<BestQuote>>, AppError> {
let quotes = subscribers
.orderbook_info()
.map(|prices| prices.get(&contract_symbol).cloned())
.and_then(|inner| inner);

Ok(Json(quotes))
Ok(Json(quotes.map(|quote| BestQuote {
price: quote,
fee: taker_fee(),
})))
}

#[derive(Serialize, Default)]
Expand Down

0 comments on commit b377029

Please sign in to comment.