Skip to content

Commit

Permalink
feat: Add wallet history
Browse files Browse the repository at this point in the history
  • Loading branch information
holzeis committed Jan 21, 2024
1 parent 463e367 commit 384d3d3
Show file tree
Hide file tree
Showing 9 changed files with 281 additions and 9 deletions.
10 changes: 10 additions & 0 deletions mobile/native/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ use ln_dlc_node::channel::UserChannelId;
use rust_decimal::prelude::FromPrimitive;
use rust_decimal::Decimal;
use std::backtrace::Backtrace;
use std::fmt;
use std::path::PathBuf;
use time::OffsetDateTime;
use tokio::sync::broadcast;
Expand Down Expand Up @@ -154,6 +155,15 @@ pub enum PaymentFlow {
Outbound,
}

impl fmt::Display for PaymentFlow {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
PaymentFlow::Inbound => write!(f, "inbound"),
PaymentFlow::Outbound => write!(f, "outbound"),
}
}
}

#[derive(Clone, Debug, Default)]
pub enum Status {
#[default]
Expand Down
49 changes: 49 additions & 0 deletions webapp/frontend/lib/common/payment.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import 'package:get_10101/common/model.dart';

class Payment {
final String address;
final int amount;
final int fee;

const Payment({required this.address, required this.amount, required this.fee});
}

enum PaymentFlow { outbound, inbound }

class OnChainPayment {
final PaymentFlow flow;
final Amount amount;
final DateTime timestamp;
final String txid;
final int confirmations;
final Amount? fee;

const OnChainPayment(
{required this.flow,
required this.amount,
required this.timestamp,
required this.txid,
required this.confirmations,
this.fee});

factory OnChainPayment.fromJson(Map<String, dynamic> json) {
return switch (json) {
{
"flow": String flow,
"amount": int amount,
"timestamp": int timestamp,
"txid": String txid,
"confirmations": int confirmations,
"fee": int fee
} =>
OnChainPayment(
flow: flow == 'inbound' ? PaymentFlow.inbound : PaymentFlow.outbound,
amount: Amount(amount),
timestamp: DateTime.fromMillisecondsSinceEpoch(timestamp * 1000),
txid: txid,
confirmations: confirmations,
fee: Amount(fee)),
_ => throw const FormatException('Failed to load history.'),
};
}
}
50 changes: 50 additions & 0 deletions webapp/frontend/lib/wallet/history_screen.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:get_10101/common/payment.dart';
import 'package:get_10101/wallet/onchain_payment_history_item.dart';
import 'package:get_10101/wallet/wallet_service.dart';
import 'package:provider/provider.dart';

class HistoryScreen extends StatefulWidget {
const HistoryScreen({super.key});

@override
State<HistoryScreen> createState() => _HistoryScreenState();
}

class _HistoryScreenState extends State<HistoryScreen> {
List<OnChainPayment> history = [];

@override
void initState() {
super.initState();
context
.read<WalletService>()
.getOnChainPaymentHistory()
.then((value) => setState(() => history = value));
}

@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.only(top: 25),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: ScrollConfiguration(
behavior: ScrollConfiguration.of(context).copyWith(
dragDevices: PointerDeviceKind.values.toSet(),
),
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Column(
children: history.map((item) => OnChainPaymentHistoryItem(data: item)).toList(),
),
),
),
),
],
));
}
}
92 changes: 92 additions & 0 deletions webapp/frontend/lib/wallet/onchain_payment_history_item.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import 'package:flutter/material.dart';
import 'package:get_10101/common/payment.dart';
import 'package:intl/intl.dart';
import 'package:timeago/timeago.dart' as timeago;

class OnChainPaymentHistoryItem extends StatelessWidget {
final OnChainPayment data;

const OnChainPaymentHistoryItem({super.key, required this.data});

@override
Widget build(BuildContext context) {
final statusIcon = switch (data.confirmations) {
>= 3 => const Icon(Icons.check_circle, color: Colors.green, size: 18),
_ => const Icon(Icons.pending, size: 18)
};

String sign = data.flow == PaymentFlow.inbound ? "+" : "-";
Color color = data.flow == PaymentFlow.inbound ? Colors.green.shade600 : Colors.red.shade600;
final flowIcon = data.flow == PaymentFlow.inbound ? Icons.arrow_downward : Icons.arrow_upward;

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

return Column(
children: [
Card(
color: Colors.transparent,
margin: const EdgeInsets.all(0),
elevation: 0,
child: ListTile(
onTap: () async {
// todo
},
leading: Stack(children: [
Container(
padding: const EdgeInsets.only(bottom: 20.0),
child: SizedBox(height: 18, width: 18, child: statusIcon),
),
Container(
padding: const EdgeInsets.only(left: 5.0, top: 10.0),
child: SizedBox(height: 30, width: 30, child: Icon(flowIcon, size: 30))),
]),
title: RichText(
overflow: TextOverflow.ellipsis,
text: TextSpan(
style: DefaultTextStyle.of(context).style,
children: const <TextSpan>[
TextSpan(text: "Payment"),
],
),
),
subtitle: RichText(
textWidthBasis: TextWidthBasis.longestLine,
text: TextSpan(style: DefaultTextStyle.of(context).style, children: <TextSpan>[
TextSpan(
text: timeago.format(data.timestamp),
style: const TextStyle(color: Colors.grey)),
])),
trailing: Padding(
padding: const EdgeInsets.only(top: 11.0, bottom: 5.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
RichText(
text: TextSpan(
style: DefaultTextStyle.of(context).style,
children: <InlineSpan>[
TextSpan(
text: "$sign${amountFormatter.format(data.amount.sats)} sats",
style: TextStyle(
color: color,
fontFamily: "Courier",
fontSize: 14,
fontWeight: FontWeight.bold))
]),
),
RichText(
text: TextSpan(
style: DefaultTextStyle.of(context).style,
children: const <TextSpan>[
TextSpan(text: "on-chain", style: TextStyle(color: Colors.grey)),
])),
],
),
)),
),
const Divider(height: 0, thickness: 0.5, indent: 10, endIndent: 10)
],
);
}
}
14 changes: 12 additions & 2 deletions webapp/frontend/lib/wallet/wallet_screen.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get_10101/common/color.dart';
import 'package:get_10101/wallet/history_screen.dart';
import 'package:get_10101/wallet/receive_screen.dart';
import 'package:get_10101/wallet/send_screen.dart';

Expand All @@ -14,7 +15,7 @@ class WalletScreen extends StatefulWidget {
}

class _WalletScreenState extends State<WalletScreen> with SingleTickerProviderStateMixin {
late final _tabController = TabController(length: 2, vsync: this);
late final _tabController = TabController(length: 3, vsync: this);

@override
Widget build(BuildContext context) {
Expand All @@ -28,6 +29,15 @@ class _WalletScreenState extends State<WalletScreen> with SingleTickerProviderSt
unselectedLabelColor: Colors.black,
labelColor: tenTenOnePurple,
tabs: const [
Tab(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(FontAwesomeIcons.clockRotateLeft, size: 20),
SizedBox(width: 10),
Text("History")
],
)),
Tab(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
Expand Down Expand Up @@ -55,7 +65,7 @@ class _WalletScreenState extends State<WalletScreen> with SingleTickerProviderSt
Expanded(
child: TabBarView(
controller: _tabController,
children: const [ReceiveScreen(), SendScreen()],
children: const [HistoryScreen(), ReceiveScreen(), SendScreen()],
),
),
],
Expand Down
28 changes: 22 additions & 6 deletions webapp/frontend/lib/wallet/wallet_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:get_10101/common/model.dart';
import 'package:get_10101/common/balance.dart';
import 'package:get_10101/common/payment.dart';
import 'package:http/http.dart' as http;

class WalletService {
Expand Down Expand Up @@ -64,12 +65,27 @@ class WalletService {
throw FlutterError("Failed to send payment. $e");
}
}
}

class Payment {
final String address;
final int amount;
final int fee;
Future<List<OnChainPayment>> getOnChainPaymentHistory() 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/history'));

const Payment({required this.address, required this.amount, required this.fee});
if (response.statusCode == 200) {
List<OnChainPayment> history = [];
Iterable list = json.decode(response.body);
for (int i = 0; i < list.length; i++) {
history.add(OnChainPayment.fromJson(list.elementAt(i)));
}
return history;
} else {
throw FlutterError("Failed to fetch onchain payment history");
}
} catch (e) {
throw FlutterError("Failed to fetch onchain payment history. $e");
}
}
}
3 changes: 2 additions & 1 deletion webapp/frontend/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,9 @@ dependencies:
font_awesome_flutter: 10.6.0
qr_flutter: ^4.0.0
bitcoin_icons: ^0.0.4
intl: ^0.19.0
intl: ^0.18.0
decimal: ^2.3.3
timeago: ^3.3.0

dev_dependencies:
flutter_test:
Expand Down
42 changes: 42 additions & 0 deletions webapp/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use axum::Json;
use native::api;
use native::api::Fee;
use native::api::SendPayment;
use native::api::WalletHistoryItemType;
use native::ln_dlc;
use serde::Deserialize;
use serde::Serialize;
Expand Down Expand Up @@ -73,6 +74,47 @@ pub async fn get_balance(
Ok(Json(balance))
}

#[derive(Serialize)]
pub struct OnChainPayment {
flow: String,
amount: u64,
timestamp: u64,
txid: String,
confirmations: u64,
fee: Option<u64>,
}

pub async fn get_onchain_payment_history(
State(subscribers): State<Arc<AppSubscribers>>,
) -> Result<Json<Vec<OnChainPayment>>, AppError> {
ln_dlc::refresh_wallet_info().await?;

let history = match subscribers.wallet_info() {
Some(wallet_info) => wallet_info
.history
.into_iter()
.filter_map(|item| match item.wallet_type {
WalletHistoryItemType::OnChain {
txid,
fee_sats,
confirmations,
} => Some(OnChainPayment {
flow: item.flow.to_string(),
amount: item.amount_sats,
timestamp: item.timestamp,
txid,
confirmations,
fee: fee_sats,
}),
_ => None,
})
.collect::<Vec<OnChainPayment>>(),
None => vec![],
};

Ok(Json(history))
}

#[derive(Deserialize)]
pub struct Payment {
address: String,
Expand Down
2 changes: 2 additions & 0 deletions webapp/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ mod logger;
mod subscribers;

use crate::api::get_balance;
use crate::api::get_onchain_payment_history;
use crate::api::get_unused_address;
use crate::api::send_payment;
use crate::api::version;
Expand Down Expand Up @@ -91,6 +92,7 @@ fn using_serve_dir(subscribers: Arc<AppSubscribers>) -> Router {
.route("/api/balance", get(get_balance))
.route("/api/newaddress", get(get_unused_address))
.route("/api/sendpayment", post(send_payment))
.route("/api/history", get(get_onchain_payment_history))
.route("/main.dart.js", get(main_dart_handler))
.route("/flutter.js", get(flutter_js))
.route("/index.html", get(index_handler))
Expand Down

0 comments on commit 384d3d3

Please sign in to comment.