2024-06-03 14:08:57 +10:00
|
|
|
import 'package:flutter/material.dart';
|
2024-06-03 17:06:45 +10:00
|
|
|
import 'package:http/http.dart' as http;
|
|
|
|
import 'dart:convert';
|
|
|
|
import 'package:url_launcher/url_launcher.dart';
|
2024-06-03 14:08:57 +10:00
|
|
|
|
|
|
|
class TransactionsPage extends StatefulWidget {
|
|
|
|
TransactionsPage({Key? key, required this.uuid, required this.wallet})
|
|
|
|
: super(key: key);
|
|
|
|
|
|
|
|
final String uuid;
|
|
|
|
String wallet;
|
|
|
|
|
|
|
|
@override
|
|
|
|
_TransactionsPageState createState() => _TransactionsPageState();
|
|
|
|
}
|
|
|
|
|
|
|
|
class _TransactionsPageState extends State<TransactionsPage> {
|
|
|
|
@override
|
|
|
|
void initState() {
|
|
|
|
super.initState();
|
2024-06-03 17:06:45 +10:00
|
|
|
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');
|
|
|
|
}
|
2024-06-03 14:08:57 +10:00
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
return Scaffold(
|
|
|
|
appBar: AppBar(
|
|
|
|
title: Text('${widget.wallet} Transactions'),
|
|
|
|
),
|
|
|
|
// Get wallet list from api and display here
|
|
|
|
body: Center(
|
|
|
|
child: LayoutBuilder(
|
|
|
|
builder: (BuildContext context, BoxConstraints constraints) {
|
2024-06-03 17:06:45 +10:00
|
|
|
return ListView(
|
2024-06-03 14:08:57 +10:00
|
|
|
children: <Widget>[
|
2024-06-03 17:06:45 +10:00
|
|
|
TextButton(
|
|
|
|
onPressed: fetchTransactions, child: Text('Refresh')),
|
|
|
|
...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)),
|
|
|
|
);
|
|
|
|
}),
|
2024-06-03 14:08:57 +10:00
|
|
|
],
|
|
|
|
);
|
|
|
|
},
|
|
|
|
),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
2024-06-03 17:06:45 +10:00
|
|
|
|
|
|
|
Future<void> openTX(String hash) async {
|
|
|
|
// Open the transaction in the browser
|
2024-06-04 14:26:35 +10:00
|
|
|
final Uri url = Uri.parse('https://hns.cymon.de/tx/$hash');
|
2024-06-03 17:06:45 +10:00
|
|
|
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;
|
|
|
|
}
|
2024-06-03 14:08:57 +10:00
|
|
|
}
|