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 (webapp): show open position #1871

Merged
merged 9 commits into from
Jan 23, 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
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Feat: update api to collaboratively revert a dlc-channel
- Feat: Allow continuing from an offered dlc channel state (offered, settle offered and collab close offered)
- Feat: add a new project `webapp`. Eventually this will have the same functionality as our app (and more) and can be run on a self-hosted server
- Chore: In Webapp API allow requests from any origin (CORS)
- Chore (webapp): Add API allow requests from any origin (CORS)
- Feat (webapp): Allow creating new orders through `webapp`
- Feat (webapp): Show open position in trade screen

## [1.7.4] - 2023-12-20

Expand Down
3 changes: 3 additions & 0 deletions Cargo.lock

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

8 changes: 6 additions & 2 deletions mobile/native/src/trade/position/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,15 @@ use rust_decimal::prelude::FromPrimitive;
use rust_decimal::prelude::ToPrimitive;
use rust_decimal::Decimal;
use rust_decimal::RoundingStrategy;
use serde::Serialize;
use time::OffsetDateTime;
use trade::ContractSymbol;
use trade::Direction;

pub mod api;
pub mod handler;

#[derive(Debug, Clone, PartialEq, Copy)]
#[derive(Debug, Clone, PartialEq, Copy, Serialize)]
pub enum PositionState {
/// The position is open
///
Expand Down Expand Up @@ -57,7 +58,7 @@ pub enum PositionState {
Resizing,
}

#[derive(Debug, Clone)]
#[derive(Debug, Clone, Serialize)]
pub struct Position {
pub leverage: f32,
pub quantity: f32,
Expand All @@ -67,8 +68,11 @@ pub struct Position {
pub liquidation_price: f32,
pub position_state: PositionState,
pub collateral: u64,
#[serde(with = "time::serde::rfc3339")]
pub expiry: OffsetDateTime,
#[serde(with = "time::serde::rfc3339")]
pub updated: OffsetDateTime,
#[serde(with = "time::serde::rfc3339")]
pub created: OffsetDateTime,
pub stable: bool,
}
Expand Down
3 changes: 3 additions & 0 deletions webapp/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,6 @@ Cargo.lock
*.pdb

# End of https://www.toptal.com/developers/gitignore/api/flutter,dart,rust

# data directory for webapp
data
Copy link
Contributor

Choose a reason for hiding this comment

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

I thought we were meaning to share the data directory with the root of the project. This is not wrong though.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yea, but if you run cargo run from the sub folder it will generate this which is not wrong but annoying to have :)

3 changes: 3 additions & 0 deletions webapp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ mime_guess = "2.0.4"
native = { path = "../mobile/native" }
parking_lot = { version = "0.12.1" }
rust-embed = "8.2.0"
rust_decimal = { version = "1", features = ["serde-with-float"] }
rust_decimal_macros = "1"
serde = "1.0.147"
serde_json = "1"
time = "0.3"
Expand All @@ -24,3 +26,4 @@ tower = { version = "0.4", features = ["util"] }
tower-http = { version = "0.5", features = ["fs", "trace", "cors"] }
tracing = "0.1.37"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
uuid = { version = "1.3.0", features = ["v4"] }
59 changes: 59 additions & 0 deletions webapp/frontend/lib/common/http_client.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import 'dart:convert';
import 'package:http/http.dart';

class HttpClientManager {
static final CustomHttpClient _httpClient = CustomHttpClient(Client(), true);

static CustomHttpClient get instance => _httpClient;
}

class CustomHttpClient extends BaseClient {
// TODO: this should come from the settings

// if this is true, we assume the website is running in dev mode and need to add _host:_port to be able to do http calls
final bool _dev;

final String _port = "3001";
final String _host = "localhost";

final Client _inner;

CustomHttpClient(this._inner, this._dev);

Future<StreamedResponse> send(BaseRequest request) {
return _inner.send(request);
}

@override
Future<Response> delete(Uri url,
{Map<String, String>? headers, Object? body, Encoding? encoding}) {
if (_dev && url.host == '') {
url = Uri.parse('http://$_host:$_port${url.toString()}');
}
return _inner.delete(url, headers: headers, body: body, encoding: encoding);
}

@override
Future<Response> put(Uri url, {Map<String, String>? headers, Object? body, Encoding? encoding}) {
if (_dev && url.host == '') {
url = Uri.parse('http://$_host:$_port${url.toString()}');
}
return _inner.put(url, headers: headers, body: body, encoding: encoding);
}

@override
Future<Response> post(Uri url, {Map<String, String>? headers, Object? body, Encoding? encoding}) {
if (_dev && url.host == '') {
url = Uri.parse('http://$_host:$_port${url.toString()}');
}
return _inner.post(url, headers: headers, body: body, encoding: encoding);
}

@override
Future<Response> get(Uri url, {Map<String, String>? headers}) {
if (_dev && url.host == '') {
url = Uri.parse('http://$_host:$_port${url.toString()}');
}
return _inner.get(url, headers: headers);
}
}
14 changes: 6 additions & 8 deletions webapp/frontend/lib/common/model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,8 @@ class Amount implements Formattable {
class Usd implements Formattable {
Decimal _usd = Decimal.zero;

Usd(int usd) {
_usd = Decimal.fromInt(usd);
Usd(double usd) {
_usd = Decimal.parse(usd.toString());
}

int get usd => _usd.toBigInt().toInt();
Expand Down Expand Up @@ -170,15 +170,13 @@ class Price implements Formattable {
}

class Leverage implements Formattable {
int _leverage = 1;

Leverage.one() : _leverage = 1;
double _leverage = 1;

int get toInt => _leverage;
Leverage.one() : _leverage = 1.0;

double get asDouble => _leverage as double;
double get asDouble => _leverage;

Leverage(int leverage) {
Leverage(double leverage) {
_leverage = leverage;
}

Expand Down
8 changes: 2 additions & 6 deletions webapp/frontend/lib/common/version_service.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'package:get_10101/common/http_client.dart';

class Version {
final String version;
Expand All @@ -21,12 +21,8 @@ class VersionService {
const VersionService();

Future<String> fetchVersion() async {
// TODO(holzeis): this should come from the config
const port = "3001";
const host = "localhost";

try {
final response = await http.get(Uri.http('$host:$port', '/api/version'));
final response = await HttpClientManager.instance.get(Uri(path: '/api/version'));

if (response.statusCode == 200) {
return Version.fromJson(jsonDecode(response.body) as Map<String, dynamic>).version;
Expand Down
43 changes: 43 additions & 0 deletions webapp/frontend/lib/trade/new_order_service.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:get_10101/common/http_client.dart';
import 'package:get_10101/common/model.dart';

class OrderId {
final String orderId;

const OrderId({required this.orderId});

factory OrderId.fromJson(Map<String, dynamic> json) {
return switch (json) {
{
'id': String orderId,
} =>
OrderId(orderId: orderId),
_ => throw const FormatException('Failed to parse order id.'),
};
}
}

class NewOrderService {
const NewOrderService();

static Future<String> postNewOrder(Leverage leverage, Usd quantity, bool isLong) async {
final response = await HttpClientManager.instance.post(Uri(path: '/api/orders'),
headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8',
},
body: jsonEncode(<String, dynamic>{
'leverage': leverage.asDouble,
'quantity': quantity.asDouble,
'direction': isLong ? "Long" : "Short",
}));

if (response.statusCode == 200) {
return OrderId.fromJson(jsonDecode(response.body) as Map<String, dynamic>).orderId;
} else {
throw FlutterError("Failed to post new order. Response ${response.body}");
}
}
}
63 changes: 63 additions & 0 deletions webapp/frontend/lib/trade/open_position_service.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import 'dart:convert';
import 'package:flutter/cupertino.dart';
import 'package:get_10101/common/http_client.dart';
import 'package:get_10101/common/model.dart';

class OpenPositionsService {
const OpenPositionsService();

static Future<List<Position>> fetchOpenPositions() async {
final response = await HttpClientManager.instance.get(Uri(path: '/api/positions'));

if (response.statusCode == 200) {
final List<dynamic> jsonData = jsonDecode(response.body);
return jsonData.map((positionData) => Position.fromJson(positionData)).toList();
} else {
throw FlutterError("Could not fetch positions");
}
}
}

class Position {
final Leverage leverage;
final Usd quantity;
final String contractSymbol;
final String direction;
final Usd averageEntryPrice;
final Usd liquidationPrice;
final String positionState;
final Amount collateral;
final DateTime expiry;
final DateTime updated;
final DateTime created;

Position({
required this.leverage,
required this.quantity,
required this.contractSymbol,
required this.direction,
required this.averageEntryPrice,
required this.liquidationPrice,
required this.positionState,
required this.collateral,
required this.expiry,
required this.updated,
required this.created,
});

factory Position.fromJson(Map<String, dynamic> json) {
return Position(
leverage: Leverage(json['leverage'] as double),
quantity: Usd(json['quantity'] as double),
contractSymbol: json['contract_symbol'] as String,
direction: json['direction'] as String,
averageEntryPrice: Usd(json['average_entry_price'] as double),
liquidationPrice: Usd(json['liquidation_price'] as double),
positionState: json['position_state'] as String,
collateral: Amount(json['collateral']),
expiry: DateTime.parse(json['expiry'] as String),
updated: DateTime.parse(json['updated'] as String),
created: DateTime.parse(json['created'] as String),
);
}
}
Loading
Loading