feat: Added tx page

This commit is contained in:
Nathan Woodburn 2024-06-03 17:06:45 +10:00
parent f0e6dcfbbe
commit f472db8e00
Signed by: nathanwoodburn
GPG Key ID: 203B000478AD0EF1
11 changed files with 496 additions and 13 deletions

View File

@ -1,4 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
class DomainsPage extends StatefulWidget { class DomainsPage extends StatefulWidget {
DomainsPage({Key? key, required this.uuid, required this.wallet}) DomainsPage({Key? key, required this.uuid, required this.wallet})
@ -15,6 +17,55 @@ class _DomainsPageState extends State<DomainsPage> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
fetchDomains();
}
List<Domain> domains = [];
Future<void> fetchDomains() async {
// Fetch domains from api
final response = await http.get(Uri.parse(
'https://api.firewallet.au/wallet/domains?uuid=${widget.uuid}&name=${widget.wallet}'));
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
if (data is Map && data.containsKey('error')) {
print('Error: ${data['error']}');
return;
}
// Return if empty
if (data.isEmpty) {
setState(() {
domains = [
Domain(
height: 0,
highest: 0,
name: 'No domains found',
state: 'CLOSED',
stats: DomainStats(unParsed: 'No domains found'),
value: 0)
];
});
return;
}
final domainsData = data as List;
setState(() {
domains = domainsData
.map((domain) => Domain(
name: domain['name'],
state: domain['state'],
height: domain['height'],
highest: domain['highest'],
stats: DomainStats(unParsed: domain['stats'].toString()),
value: domain['value'],
))
.toList();
});
} else {
// Handle error
print('Failed to load wallet names');
}
} }
@override @override
@ -27,12 +78,18 @@ class _DomainsPageState extends State<DomainsPage> {
body: Center( body: Center(
child: LayoutBuilder( child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) { builder: (BuildContext context, BoxConstraints constraints) {
return Column( return ListView(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[ children: <Widget>[
Text("Domain 1"), ...domains.map((name) {
Text("Domain 2"), return Card(
Text("Domain 3"), child: ListTile(
title: Text(name.name),
subtitle: Text(name.state),
trailing: Text(name.value.toString()),
),
);
}),
TextButton(onPressed: fetchDomains, child: Text('Refresh')),
], ],
); );
}, },
@ -41,3 +98,27 @@ class _DomainsPageState extends State<DomainsPage> {
); );
} }
} }
class Domain {
final String name;
final String state;
final int height;
final double highest;
final DomainStats stats;
final double value;
Domain({
required this.name,
required this.state,
required this.height,
required this.highest,
required this.stats,
required this.value,
});
}
class DomainStats {
final String unParsed;
DomainStats({required this.unParsed});
}

View File

@ -21,12 +21,15 @@ class IndexPage extends StatefulWidget {
class _IndexPageState extends State<IndexPage> { class _IndexPageState extends State<IndexPage> {
late String wallet; late String wallet;
List<String> walletNames = []; List<String> walletNames = [];
late double walletBalance;
bool balanceLoaded = false;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
wallet = widget.wallet; wallet = widget.wallet;
fetchWalletNames(); fetchWalletNames();
fetchWalletBalance();
} }
Future<void> fetchWalletNames() async { Future<void> fetchWalletNames() async {
@ -108,19 +111,53 @@ class _IndexPageState extends State<IndexPage> {
void setWallet(String s) { void setWallet(String s) {
setState(() { setState(() {
wallet = s; wallet = s;
balanceLoaded = false;
}); });
widget.setWallet(s); widget.setWallet(s);
} }
int getWalletBalance() { Future<void> fetchWalletBalance() async {
return 100; if (wallet.isEmpty) {
walletBalance = 0;
return;
}
if (balanceLoaded) {
return;
}
final response = await http.get(Uri.parse(
'https://api.firewallet.au/wallet/balance?uuid=${widget.uuid}&name=$wallet'));
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
print(data);
setState(() {
balanceLoaded = true;
walletBalance = data['available'];
});
} else {
// Handle error
print('Failed to load wallet balance');
}
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text('$wallet - ${getWalletBalance()} HNS'), title: FutureBuilder<void>(
future: fetchWalletBalance(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Text('$wallet - Loading...');
} else if (snapshot.hasError) {
return Text('$wallet - Error');
} else {
return Text('$wallet - ${walletBalance.toStringAsFixed(2)} HNS');
}
},
),
), ),
body: Center( body: Center(
child: LayoutBuilder( child: LayoutBuilder(

View File

@ -31,6 +31,7 @@ Future<void> main() async {
// Get the uuid from the shared preferences // Get the uuid from the shared preferences
final String? uuid = prefs.getString('uuid'); final String? uuid = prefs.getString('uuid');
print('UUID: $uuid');
runApp(MyApp(uuid: uuid ?? 'null')); runApp(MyApp(uuid: uuid ?? 'null'));
} }

View File

@ -1,4 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'package:url_launcher/url_launcher.dart';
class TransactionsPage extends StatefulWidget { class TransactionsPage extends StatefulWidget {
TransactionsPage({Key? key, required this.uuid, required this.wallet}) TransactionsPage({Key? key, required this.uuid, required this.wallet})
@ -15,6 +18,87 @@ class _TransactionsPageState extends State<TransactionsPage> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
fetchTransactions();
}
List<Transaction> transactions = [];
Future<void> fetchTransactions() async {
// Fetch domains from api
final response = await http.get(Uri.parse(
'https://api.firewallet.au/wallet/transactions?uuid=${widget.uuid}&name=${widget.wallet}'));
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
if (data is Map && data.containsKey('error')) {
print('Error: ${data['error']}');
return;
}
// Return if empty
if (data.isEmpty) {
setState(() {
transactions = [
Transaction(
confirmations: 0,
date: 'No transactions found',
hash: 'No transactions found',
inputs: [],
outputs: [])
];
});
return;
}
final txData = data as List;
setState(() {
transactions = txData
.map(
(tx) => Transaction(
confirmations: tx['confirmations'],
date: tx['date'],
hash: tx['hash'],
inputs: (tx['inputs'] as List<dynamic>).map<TXInput>((input) {
return TXInput(
address: input['address'],
value: input['value'],
path: input['path'] == null
? Path()
: Path(
account: input['path']['account'],
change: input['path']['change'],
derivation: input['path']['derivation'],
name: input['path']['name'],
));
}).toList(),
outputs:
(tx['outputs'] as List<dynamic>).map<TXOutput>((output) {
return TXOutput(
address: output['address'],
value: output['value'],
covenant: Covenant(
action: output['covenant']['action'],
type: output['covenant']['type'],
items: List<String>.from(output['covenant']['items'])),
path: output['path'] == null
? Path()
: Path(
account: output['path']['account'],
change: output['path']['change'],
derivation: output['path']['derivation'],
name: output['path']['name'],
),
);
}).toList(),
),
)
.toList();
});
print(transactions[0].hash);
print(transactions[0].value().toStringAsFixed(2));
} else {
// Handle error
print('Failed to load wallet names');
}
} }
@override @override
@ -27,12 +111,20 @@ class _TransactionsPageState extends State<TransactionsPage> {
body: Center( body: Center(
child: LayoutBuilder( child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) { builder: (BuildContext context, BoxConstraints constraints) {
return Column( return ListView(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[ children: <Widget>[
Text("Domain 1"), TextButton(
Text("Domain 2"), onPressed: fetchTransactions, child: Text('Refresh')),
Text("Domain 3"), ...transactions.map((tx) {
return Card(
child: ListTile(
title: Text(tx.toString()),
subtitle: Text(tx.hash),
trailing: Text('${tx.value().toStringAsFixed(2)} HNS'),
onTap: // Open tx in browser
() => openTX(tx.hash)),
);
}),
], ],
); );
}, },
@ -40,4 +132,200 @@ class _TransactionsPageState extends State<TransactionsPage> {
), ),
); );
} }
Future<void> openTX(String hash) async {
// Open the transaction in the browser
final Uri url = Uri.parse('https://flutter.dev');
if (!await launchUrl(url)) {
throw Exception('Could not launch $url');
}
}
}
class Transaction {
final int confirmations;
final String date;
final String hash;
final List<TXInput> inputs;
final List<TXOutput> outputs;
double totalValue = 0;
bool valueCalculated = false;
Transaction(
{required this.confirmations,
required this.date,
required this.hash,
required this.inputs,
required this.outputs});
// Calculate the total value of the transaction
double value() {
if (valueCalculated) {
return totalValue / 1000000;
}
totalValue = inputs.fold(0, (prev, input) => prev + input.getValue());
totalValue += outputs.fold(0, (prev, output) => prev + output.getValue());
valueCalculated = true;
return totalValue / 1000000;
}
@override
String toString() {
if (inputs.isEmpty) {
return 'No Transactions Found';
}
int opens = 0;
int bids = 0;
int reveals = 0;
int redeems = 0;
int registers = 0;
int updates = 0;
int transfers = 0;
int finalizes = 0;
int renews = 0;
int other = 0;
outputs.forEach((output) {
if (output.covenant.action == 'OPEN') {
opens++;
} else if (output.covenant.action == 'BID') {
bids++;
} else if (output.covenant.action == 'REVEAL') {
reveals++;
} else if (output.covenant.action == 'REDEEM') {
redeems++;
} else if (output.covenant.action == 'REGISTER') {
registers++;
} else if (output.covenant.action == 'UPDATE') {
updates++;
} else if (output.covenant.action == 'TRANSFER') {
transfers++;
} else if (output.covenant.action == 'FINALIZE') {
finalizes++;
} else if (output.covenant.action == 'RENEW') {
renews++;
} else if (output.covenant.action == 'NONE') {
other++;
} else {
other++;
print('Unknown action: ${output.covenant.action}');
}
});
String outputString = '';
if (opens > 0) {
outputString += 'Opens: $opens, ';
}
if (bids > 0) {
outputString += 'Bids: $bids, ';
}
if (reveals > 0) {
outputString += 'Reveals: $reveals, ';
}
if (redeems > 0) {
outputString += 'Redeems: $redeems, ';
}
if (registers > 0) {
outputString += 'Registers: $registers, ';
}
if (updates > 0) {
outputString += 'Updates: $updates, ';
}
if (transfers > 0) {
outputString += 'Transfers: $transfers, ';
}
if (finalizes > 0) {
outputString += 'Finalizes: $finalizes, ';
}
if (renews > 0) {
outputString += 'Renews: $renews, ';
}
// Remove trailing comma
if (outputString.isNotEmpty) {
outputString = outputString.substring(0, outputString.length - 2);
}
if (outputString.isEmpty) {
outputString = 'Sent HNS';
}
return outputString;
}
}
class TXInput {
final String address;
final int value;
final Path path;
TXInput({required this.address, required this.value, required this.path});
int getValue() {
if (path.userPath()) {
return value * -1;
}
return value;
}
}
class TXOutput {
final String address;
final int value;
final Covenant covenant;
final Path path;
TXOutput(
{required this.address,
required this.value,
required this.covenant,
required this.path});
int getValue() {
if (path.userPath()) {
if (covenant.action == 'BID') {
return 0;
}
return value;
}
return 0;
}
}
class Covenant {
final String action;
final int type;
final List<String> items;
Covenant({required this.action, required this.type, required this.items});
}
class Path {
int? account;
bool? change;
String? derivation;
String? name;
Path({this.account, this.change, this.derivation, this.name});
@override
String toString() {
if (name == null) {
return 'No Path Found';
}
if (derivation == null) {
return 'Path(name: $name)';
}
return 'Path(account: $account, change: $change, derivation: $derivation, name: $name)';
}
bool userPath() {
return derivation != null;
}
} }

View File

@ -6,6 +6,10 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <url_launcher_linux/url_launcher_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) { void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
} }

View File

@ -3,6 +3,7 @@
# #
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
url_launcher_linux
) )
list(APPEND FLUTTER_FFI_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST

View File

@ -6,7 +6,9 @@ import FlutterMacOS
import Foundation import Foundation
import shared_preferences_foundation import shared_preferences_foundation
import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
} }

View File

@ -405,6 +405,70 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.2" version: "1.3.2"
url_launcher:
dependency: "direct main"
description:
name: url_launcher
sha256: "6ce1e04375be4eed30548f10a315826fd933c1e493206eab82eed01f438c8d2e"
url: "https://pub.dev"
source: hosted
version: "6.2.6"
url_launcher_android:
dependency: transitive
description:
name: url_launcher_android
sha256: ceb2625f0c24ade6ef6778d1de0b2e44f2db71fded235eb52295247feba8c5cf
url: "https://pub.dev"
source: hosted
version: "6.3.3"
url_launcher_ios:
dependency: transitive
description:
name: url_launcher_ios
sha256: "7068716403343f6ba4969b4173cbf3b84fc768042124bc2c011e5d782b24fe89"
url: "https://pub.dev"
source: hosted
version: "6.3.0"
url_launcher_linux:
dependency: transitive
description:
name: url_launcher_linux
sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811
url: "https://pub.dev"
source: hosted
version: "3.1.1"
url_launcher_macos:
dependency: transitive
description:
name: url_launcher_macos
sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de"
url: "https://pub.dev"
source: hosted
version: "3.2.0"
url_launcher_platform_interface:
dependency: transitive
description:
name: url_launcher_platform_interface
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
url_launcher_web:
dependency: transitive
description:
name: url_launcher_web
sha256: "8d9e750d8c9338601e709cd0885f95825086bd8b642547f26bda435aade95d8a"
url: "https://pub.dev"
source: hosted
version: "2.3.1"
url_launcher_windows:
dependency: transitive
description:
name: url_launcher_windows
sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7
url: "https://pub.dev"
source: hosted
version: "3.1.1"
vector_math: vector_math:
dependency: transitive dependency: transitive
description: description:

View File

@ -37,6 +37,7 @@ dependencies:
cupertino_icons: ^1.0.6 cupertino_icons: ^1.0.6
shared_preferences: ^2.2.3 shared_preferences: ^2.2.3
http: ^1.2.1 http: ^1.2.1
url_launcher: ^6.2.6
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View File

@ -6,6 +6,9 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) { void RegisterPlugins(flutter::PluginRegistry* registry) {
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
} }

View File

@ -3,6 +3,7 @@
# #
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
url_launcher_windows
) )
list(APPEND FLUTTER_FFI_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST