Skip to content
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
42 changes: 4 additions & 38 deletions .github/workflows/flutter-desktop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -198,48 +198,12 @@ jobs:
- name: Upload to Release
if: github.event_name == 'release'
uses: softprops/action-gh-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
files: build/linux/x64/release/bundle/XF₲-Wallet-Linux-GLIBC-2.31.tar.gz
name: XF₲-Wallet-Linux-GLIBC-2.31.tar.gz

- name: Create AppImage (GLIBC 2.35)
run: |
cd build/linux/x64/release/bundle
# Download appimagetool
curl -LO https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage
chmod +x appimagetool-x86_64.AppImage
# Create desktop file for AppImage
cat > XF₲-Wallet.desktop << 'EOF'
[Desktop Entry]
Type=Application
Name=XF₲ Wallet
Comment=Privacy-focused cryptocurrency wallet
Exec=fuego_wallet
Icon=fuego_wallet
Categories=Finance;
EOF
# Copy icon file
cp ../../../../../app_icon_256.png fuego_wallet.png
# Create AppImage (using extraction to avoid FUSE)
./appimagetool-x86_64.AppImage --appimage-extract
./squashfs-root/AppRun . XF₲-Wallet-Linux-GLIBC-2.35.AppImage
# Clean up
rm -f appimagetool-x86_64.AppImage

- name: Upload AppImage artifacts
uses: actions/upload-artifact@v4
with:
name: xfg-wallet-linux-glibc-235-appimage
path: build/linux/x64/release/bundle/XF₲-Wallet-Linux-GLIBC-2.35.AppImage
retention-days: 30

- name: Upload AppImage to Release
if: github.event_name == 'release'
uses: softprops/action-gh-release@v1
with:
files: build/linux/x64/release/bundle/XF₲-Wallet-Linux-GLIBC-2.35.AppImage
name: XF₲-Wallet-Linux-GLIBC-2.35.AppImage


build-linux-latest:
name: Build XF₲ Wallet (Linux - Latest)
Expand Down Expand Up @@ -289,6 +253,8 @@ jobs:
- name: Upload to Release
if: github.event_name == 'release'
uses: softprops/action-gh-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
files: build/linux/x64/release/bundle/XF₲-Wallet-Linux-Latest.tar.gz
name: XF₲-Wallet-Linux-Latest.tar.gz
96 changes: 96 additions & 0 deletions lib/models/transaction_model.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
class TransactionModel {
final String id;
final String fromAddress;
final String? toAddress;
final String amount;
final String fee;
final String timestamp;
final String status;
final String type;
final String? txHash;
final String? memo;

TransactionModel({
required this.id,
required this.fromAddress,
this.toAddress,
required this.amount,
required this.fee,
required this.timestamp,
required this.status,
required this.type,
this.txHash,
this.memo,
});

factory TransactionModel.fromJson(Map<String, dynamic> json) {
return TransactionModel(
id: json['id'] ?? '',
fromAddress: json['from_address'] ?? '',
toAddress: json['to_address'],
amount: json['amount'] ?? '0',
fee: json['fee'] ?? '0',
timestamp: json['timestamp'] ?? '',
status: json['status'] ?? 'pending',
type: json['type'] ?? 'transfer',
txHash: json['tx_hash'],
memo: json['memo'],
);
}

Map<String, dynamic> toJson() {
return {
'id': id,
'from_address': fromAddress,
'to_address': toAddress,
'amount': amount,
'fee': fee,
'timestamp': timestamp,
'status': status,
'type': type,
'tx_hash': txHash,
'memo': memo,
};
}

/// Check if this is a burn transaction
bool get isBurnTransaction {
return type == 'burn' || toAddress == null || toAddress!.isEmpty;
}

/// Get formatted amount for display
String get formattedAmount {
final amountDouble = double.tryParse(amount) ?? 0.0;
return '${amountDouble.toStringAsFixed(8)} XFG';
}

/// Get formatted fee for display
String get formattedFee {
final feeDouble = double.tryParse(fee) ?? 0.0;
return '${feeDouble.toStringAsFixed(8)} XFG';
}

/// Get formatted timestamp for display
String get formattedTimestamp {
try {
final dateTime = DateTime.fromMillisecondsSinceEpoch(int.parse(timestamp));
return '${dateTime.day}/${dateTime.month}/${dateTime.year} ${dateTime.hour}:${dateTime.minute.toString().padLeft(2, '0')}';
} catch (e) {
return timestamp;
}
}

@override
String toString() {
return 'TransactionModel(id: $id, from: $fromAddress, to: $toAddress, amount: $amount, type: $type)';
}

@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is TransactionModel && other.id == id;
}

@override
int get hashCode => id.hashCode;
}
59 changes: 59 additions & 0 deletions lib/providers/wallet_provider.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:logging/logging.dart';
import '../models/wallet.dart';
import '../services/fuego_rpc_service.dart';
import '../services/security_service.dart';

class WalletProvider extends ChangeNotifier {
static final Logger _logger = Logger('WalletProvider');
final FuegoRPCService _rpcService;
final SecurityService _securityService;

Expand Down Expand Up @@ -53,6 +55,63 @@ class WalletProvider extends ChangeNotifier {
bool get isWalletSynced => _wallet?.synced ?? false;
double get syncProgress => _wallet?.syncProgress ?? 0.0;

// Get private key for burn transactions (requires PIN verification)
Future<String?> getPrivateKeyForBurn(String pin) async {
try {
_logger.info('Attempting to get private key for burn transaction');

final isValidPin = await _securityService.verifyPIN(pin);
if (!isValidPin) {
_logger.warning('Invalid PIN provided for private key access');
throw Exception('Invalid PIN');
}

final keys = await _securityService.getWalletKeys(pin);
if (keys == null || _wallet == null) {
_logger.severe('Wallet keys not found');
throw Exception('Wallet keys not found');
}

_logger.info('Private key accessed successfully for burn transaction');
// Return the spend key as the private key for burn transactions
return keys['spendKey'];
} catch (e) {
_logger.severe('Failed to get private key: $e');
_setError('Failed to get private key: $e');
return null;
}
}

// Get private key without PIN verification (for internal use when wallet is unlocked)
String? getPrivateKey() {
if (_wallet == null) {
_setError('Wallet not loaded');
return null;
}

// Only return private key if wallet is synced and unlocked
if (!isWalletSynced) {
_setError('Wallet must be synced to access private key');
return null;
}

return _wallet?.spendKey;
}

// Validate private key format (basic validation)
bool isValidPrivateKey(String privateKey) {
// Basic validation - in real implementation, this would validate against Fuego key format
return privateKey.isNotEmpty && privateKey.length >= 32;
}

// Clear sensitive data from memory
void clearSensitiveData() {
// In a real implementation, this would securely clear memory
// For now, we'll just clear the wallet reference
_wallet = null;
notifyListeners();
}

// Initialize connectivity monitoring
void _initConnectivity() {
Connectivity().onConnectivityChanged.listen((ConnectivityResult result) {
Expand Down
124 changes: 103 additions & 21 deletions lib/screens/banking/banking_screen.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
-import 'package:flutter/material.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../providers/wallet_provider.dart';
import '../../utils/theme.dart';
Expand Down Expand Up @@ -59,36 +59,76 @@ class _BankingScreenState extends State<BankingScreen>
// Get wallet provider for private key
final walletProvider = Provider.of<WalletProvider>(context, listen: false);

// For now, use a placeholder private key - in real implementation,
// this would come from the wallet's private key
const String privateKey = 'placeholder_private_key';
// Get the actual private key from the wallet provider
final walletProvider = Provider.of<WalletProvider>(context, listen: false);
final wallet = walletProvider.wallet;

if (wallet == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Wallet not loaded. Please unlock your wallet first.'),
backgroundColor: Colors.red,
),
);
return;
}

// Try to get private key directly first (if wallet is unlocked)
String? privateKey = walletProvider.getPrivateKey();

// If not available, prompt for PIN
if (privateKey == null) {
final pin = await _showPinDialog(context);
if (pin == null) {
return; // User cancelled
}

// Get private key with PIN verification
privateKey = await walletProvider.getPrivateKeyForBurn(pin);
if (privateKey == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Failed to access private key. Please try again.'),
backgroundColor: Colors.red,
),
);
return;
}
}

// Validate private key format
if (!walletProvider.isValidPrivateKey(privateKey)) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Invalid private key format. Please check your wallet.'),
backgroundColor: Colors.red,
),
);
return;
}

const String recipientAddress = '0x0000000000000000000000000000000000000000';

// Generate STARK proof using CLI
final Map<String, dynamic> result = await CLIService.generateBurnProof(
privateKey: privateKey,
final BurnProofResult result = await CLIService.generateBurnProof(
transactionHash: 'burn_${DateTime.now().millisecondsSinceEpoch}', // Generate a unique transaction hash
burnAmount: burnAmount,
recipientAddress: recipientAddress,
);

// Close loading dialog
Navigator.of(context).pop();

if (result['success']) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Successfully burned $burnAmount XFG to mint $heatAmount Ξmbers'),
backgroundColor: Colors.green,
),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Burn failed: ${result['error']}'),
backgroundColor: Colors.red,
),
);
}
// Show success message
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Successfully burned $burnAmount XFG to mint $heatAmount Ξmbers\nProof Hash: ${result.proofHash}'),
backgroundColor: Colors.green,
),
);

// Clear sensitive data from memory
privateKey = null;
} catch (e) {
// Close loading dialog
Navigator.of(context).pop();
Expand All @@ -102,6 +142,48 @@ class _BankingScreenState extends State<BankingScreen>
}
}

// Show PIN dialog for private key access
Future<String?> _showPinDialog(BuildContext context) async {
final pinController = TextEditingController();

return showDialog<String>(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Enter PIN'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text('Please enter your PIN to access your private key for the burn transaction:'),
const SizedBox(height: 16),
TextField(
controller: pinController,
obscureText: true,
keyboardType: TextInputType.number,
maxLength: 6,
decoration: const InputDecoration(
labelText: 'PIN',
border: OutlineInputBorder(),
),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Cancel'),
),
ElevatedButton(
onPressed: () => Navigator.of(context).pop(pinController.text),
child: const Text('Confirm'),
),
],
);
},
);
}

@override
Widget build(BuildContext context) {
return Scaffold(
Expand Down
Loading
Loading