Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add min margin to channel trade constraints #1906

Merged
merged 5 commits into from
Jan 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

- Chore: Set minimum quantity to 1 if dlc channel is open
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Won't this result in tiny DLCs that cannot be claimed on-chain?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Won't this result in tiny DLCs that cannot be claimed on-chain?

Aren't we considering the full amount on the CET transaction including any potential off chain amount.

So I think we only have to care about the initial margin to be big enough for a channel but afterwards there shouldn't be a limit.

Either party may loose so much that their payout can't be claimed anymore. But I don't think that's a problem.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you are right!

I do think eventually we should track whether either side is getting too close to the dust limit, to at least inform about this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do think eventually we should track whether either side is getting too close to the dust limit, to at least inform about this.

afaik rust-dlc is taking care of this.

- Chore: Enforce minimum margin of 250k instead of min quantity

## [1.8.2] - 2024-01-26

- Feat: Add endpoint to force close ln-dlc channels
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion mobile/lib/common/amount_text_field.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import 'package:flutter/material.dart';
import 'package:get_10101/common/domain/model.dart';

class AmountTextField extends StatefulWidget {
const AmountTextField({super.key, required this.label, required this.value, this.suffixIcon});
const AmountTextField(
{super.key, required this.label, required this.value, this.suffixIcon, this.error});

final Amount value;
final String label;
final Widget? suffixIcon;
final String? error;

@override
State<AmountTextField> createState() => _AmountTextState();
Expand All @@ -22,6 +24,10 @@ class _AmountTextState extends State<AmountTextField> {
border: const OutlineInputBorder(),
labelText: widget.label,
labelStyle: const TextStyle(color: Colors.black87),
errorStyle: TextStyle(
color: Colors.red[900],
),
errorText: widget.error,
filled: true,
suffixIcon: widget.suffixIcon,
fillColor: Colors.grey[50],
Expand Down
4 changes: 4 additions & 0 deletions mobile/lib/common/application/channel_info_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import 'package:get_10101/ffi.dart' as rust;
class ChannelInfoService {
const ChannelInfoService();

rust.TradeConstraints getTradeConstraints() {
return rust.api.channelTradeConstraints();
}

Future<ChannelInfo?> getChannelInfo() async {
rust.ChannelInfo? channelInfo = await rust.api.channelInfo();
return channelInfo != null ? ChannelInfo.fromApi(channelInfo) : null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ class TradeValuesService {
}
}

Amount? orderMatchingFee({required Amount? quantity, required double? price}) {
return quantity != null && price != null
? Amount(rust.api.orderMatchingFee(quantity: quantity.asDouble(), price: price))
: null;
}

DateTime getExpiryTimestamp() {
String network = const String.fromEnvironment('NETWORK', defaultValue: "regtest");
return DateTime.fromMillisecondsSinceEpoch(
Expand Down
13 changes: 3 additions & 10 deletions mobile/lib/features/trade/domain/trade_values.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import 'package:get_10101/common/domain/model.dart';
import 'package:get_10101/features/trade/application/trade_values_service.dart';
import 'package:get_10101/features/trade/domain/direction.dart';
import 'package:get_10101/features/trade/domain/leverage.dart';
import 'package:get_10101/ffi.dart' as rust;

class TradeValues {
Amount? margin;
Expand Down Expand Up @@ -47,7 +46,7 @@ class TradeValues {
price: price, leverage: leverage, direction: direction)
: null;

Amount? fee = orderMatchingFee(quantity, price);
Amount? fee = tradeValuesService.orderMatchingFee(quantity: quantity, price: price);

DateTime expiry = tradeValuesService.getExpiryTimestamp();

Expand Down Expand Up @@ -78,7 +77,7 @@ class TradeValues {
price: price, leverage: leverage, direction: direction)
: null;

Amount? fee = orderMatchingFee(quantity, price);
Amount? fee = tradeValuesService.orderMatchingFee(quantity: quantity, price: price);

DateTime expiry = tradeValuesService.getExpiryTimestamp();

Expand Down Expand Up @@ -146,12 +145,6 @@ class TradeValues {
}

_recalculateFee() {
fee = orderMatchingFee(quantity, price);
fee = tradeValuesService.orderMatchingFee(quantity: quantity, price: price);
}
}

Amount? orderMatchingFee(Amount? quantity, double? price) {
return quantity != null && price != null
? Amount(rust.api.orderMatchingFee(quantity: quantity.asDouble(), price: price))
: null;
}
81 changes: 22 additions & 59 deletions mobile/lib/features/trade/trade_bottom_sheet_tab.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import 'dart:async';

import 'package:flutter/material.dart';
import 'package:get_10101/common/amount_text.dart';
import 'package:get_10101/common/amount_text_field.dart';
Expand All @@ -22,6 +20,7 @@ import 'package:get_10101/features/trade/trade_dialog.dart';
import 'package:get_10101/features/trade/trade_theme.dart';
import 'package:get_10101/features/trade/trade_value_change_notifier.dart';
import 'package:go_router/go_router.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';

const contractSymbol = ContractSymbol.btcusd;
Expand Down Expand Up @@ -57,13 +56,6 @@ class _TradeBottomSheetTabState extends State<TradeBottomSheetTab> {
super.initState();
}

Future<rust.TradeConstraints> _getTradeConstraints() async {
var completer = Completer<rust.TradeConstraints>();
var channelTradeConstraints = rust.api.channelTradeConstraints();
completer.complete(channelTradeConstraints);
return completer.future;
}

@override
void dispose() {
marginController.dispose();
Expand All @@ -81,69 +73,35 @@ class _TradeBottomSheetTabState extends State<TradeBottomSheetTab> {
String label = direction == Direction.long ? "Buy" : "Sell";
Color color = direction == Direction.long ? tradeTheme.buy : tradeTheme.sell;

final channelInfoService = lspChangeNotifier.channelInfoService;
final channelTradeConstraints = channelInfoService.getTradeConstraints();

return Form(
key: _formKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
FutureBuilder<rust.TradeConstraints>(
future: _getTradeConstraints(), // a previously-obtained Future<String> or null
builder: (BuildContext context, AsyncSnapshot<rust.TradeConstraints> snapshot) {
List<Widget> children;

final channelInfoService = lspChangeNotifier.channelInfoService;

if (snapshot.hasData) {
var tradeConstraints = snapshot.data!;

children = <Widget>[
buildChildren(direction, tradeConstraints, context, channelInfoService, _formKey),
];
} else if (snapshot.hasError) {
children = <Widget>[
const Icon(
Icons.error_outline,
color: Colors.red,
size: 60,
),
Padding(
padding: const EdgeInsets.only(top: 16),
child:
Text('Error: Could not load confirmation screen due to ${snapshot.error}'),
),
];
} else {
children = const <Widget>[
SizedBox(
width: 60,
height: 60,
child: CircularProgressIndicator(),
),
Padding(
padding: EdgeInsets.only(top: 16),
child: Text('Loading confirmation screen...'),
),
];
}
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: children,
),
);
},
Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
buildChildren(
direction, channelTradeConstraints, context, channelInfoService, _formKey)
],
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
ElevatedButton(
key: widget.buttonKey,
onPressed: () {
if (_formKey.currentState!.validate()) {
TradeValues tradeValues =
context.read<TradeValuesChangeNotifier>().fromDirection(direction);
TradeValues tradeValues =
context.read<TradeValuesChangeNotifier>().fromDirection(direction);
if (_formKey.currentState!.validate() &&
channelTradeConstraints.minMargin <= (tradeValues.margin?.sats ?? 0)) {
final submitOrderChangeNotifier = context.read<SubmitOrderChangeNotifier>();
tradeBottomSheetConfirmation(
context: context,
Expand Down Expand Up @@ -209,14 +167,16 @@ class _TradeBottomSheetTabState extends State<TradeBottomSheetTab> {
"\nWith your current balance, the maximum you can trade is ${formatUsd(Usd(maxQuantity.toInt()))}";
}

var amountFormatter = NumberFormat.compact(locale: "en_UK");

return Wrap(
runSpacing: 12,
children: [
Padding(
padding: const EdgeInsets.only(bottom: 10),
child: Row(
children: [
const Flexible(child: Text("Usable Balance:")),
const Flexible(child: Text("Balance:")),
const SizedBox(width: 5),
Flexible(child: AmountText(amount: Amount(usableBalance))),
const SizedBox(
Expand Down Expand Up @@ -319,6 +279,9 @@ class _TradeBottomSheetTabState extends State<TradeBottomSheetTab> {
return AmountTextField(
value: margin,
label: "Margin (sats)",
error: channelTradeConstraints.minMargin > margin.sats
? "Min margin is ${amountFormatter.format(channelTradeConstraints.minMargin)} sats"
: null,
suffixIcon: showCapacityInfo
? ModalBottomSheetInfo(
closeButtonText: "Back to order",
Expand Down
6 changes: 4 additions & 2 deletions mobile/native/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -457,11 +457,13 @@ pub struct TradeConstraints {
/// has in the channel. In the future we can consider splice in and allow the user to use more
/// than just his channel balance.
pub is_channel_balance: bool,
/// Smallest allowed margin
pub min_margin: u64,
}

pub fn channel_trade_constraints() -> Result<TradeConstraints> {
pub fn channel_trade_constraints() -> Result<SyncReturn<TradeConstraints>> {
let trade_constraints = channel_trade_constraints::channel_trade_constraints()?;
Ok(trade_constraints)
Ok(SyncReturn(trade_constraints))
}

pub fn max_channel_value() -> Result<u64> {
Expand Down
5 changes: 4 additions & 1 deletion mobile/native/src/channel_trade_constraints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ pub fn channel_trade_constraints() -> Result<TradeConstraints> {

// TODO(bonomat): retrieve these values from the coordinator. This can come from the liquidity
// options.
let min_quantity = signed_channel.map(|_| 100).unwrap_or(500);
let min_quantity = 1;
let min_margin = signed_channel.map(|_| 1).unwrap_or(250_000);

// TODO(bonomat): this logic should be removed once we have our liquidity options again and the
// on-boarding logic. For now we take the highest liquidity option
Expand Down Expand Up @@ -40,6 +41,7 @@ pub fn channel_trade_constraints() -> Result<TradeConstraints> {
coordinator_leverage,
min_quantity,
is_channel_balance: false,
min_margin,
}
}
Some(channel) => TradeConstraints {
Expand All @@ -48,6 +50,7 @@ pub fn channel_trade_constraints() -> Result<TradeConstraints> {
coordinator_leverage,
min_quantity,
is_channel_balance: true,
min_margin,
},
};
Ok(trade_constraints)
Expand Down
21 changes: 19 additions & 2 deletions mobile/test/trade_test.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:candlesticks/candlesticks.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:get_10101/bridge_generated/bridge_definitions.dart' as bridge;
import 'package:get_10101/common/amount_denomination_change_notifier.dart';
@GenerateNiceMocks([MockSpec<ChannelInfoService>()])
import 'package:get_10101/common/application/channel_info_service.dart';
Expand Down Expand Up @@ -96,6 +97,9 @@ void main() {
price: anyNamed('price'), leverage: anyNamed('leverage'), margin: anyNamed('margin')))
.thenReturn(Amount(1));
when(tradeValueService.getExpiryTimestamp()).thenReturn(DateTime.now());
when(tradeValueService.orderMatchingFee(
quantity: anyNamed('quantity'), price: anyNamed('price')))
.thenReturn(Amount(42));

// assuming this is an initial funding, no channel exists yet
when(channelConstraintsService.getChannelInfo()).thenAnswer((_) async {
Expand All @@ -114,6 +118,15 @@ void main() {
return 1;
});

when(channelConstraintsService.getTradeConstraints()).thenAnswer((_) =>
const bridge.TradeConstraints(
maxLocalMarginSats: 20000000000,
maxCounterpartyMarginSats: 200000000000,
coordinatorLeverage: 2,
minQuantity: 1,
isChannelBalance: true,
minMargin: 1));

when(candlestickService.fetchCandles(1000)).thenAnswer((_) async {
return getDummyCandles(1000);
});
Expand All @@ -133,11 +146,15 @@ void main() {

LspChangeNotifier lspChangeNotifier = LspChangeNotifier(channelConstraintsService);

final tradeValuesChangeNotifier = TradeValuesChangeNotifier(tradeValueService);

final price = Price(bid: 30000.0, ask: 30000.0);
// We have to have current price, otherwise we can't take order
positionChangeNotifier.price = Price(bid: 30000.0, ask: 30000.0);
positionChangeNotifier.price = price;
tradeValuesChangeNotifier.updatePrice(price);

await tester.pumpWidget(MultiProvider(providers: [
ChangeNotifierProvider(create: (context) => TradeValuesChangeNotifier(tradeValueService)),
ChangeNotifierProvider(create: (context) => tradeValuesChangeNotifier),
ChangeNotifierProvider(create: (context) => submitOrderChangeNotifier),
ChangeNotifierProvider(create: (context) => OrderChangeNotifier(orderService)),
ChangeNotifierProvider(create: (context) => positionChangeNotifier),
Expand Down
Loading