diff --git a/.github/workflows/flutter-desktop.yml b/.github/workflows/flutter-desktop.yml index 99c8715..b3ae414 100644 --- a/.github/workflows/flutter-desktop.yml +++ b/.github/workflows/flutter-desktop.yml @@ -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) @@ -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 diff --git a/lib/models/transaction_model.dart b/lib/models/transaction_model.dart new file mode 100644 index 0000000..4e57ef5 --- /dev/null +++ b/lib/models/transaction_model.dart @@ -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 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 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; +} \ No newline at end of file diff --git a/lib/providers/wallet_provider.dart b/lib/providers/wallet_provider.dart index 661a64d..0de63dd 100644 --- a/lib/providers/wallet_provider.dart +++ b/lib/providers/wallet_provider.dart @@ -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; @@ -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 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) { diff --git a/lib/screens/banking/banking_screen.dart b/lib/screens/banking/banking_screen.dart index 9334148..d1dc789 100644 --- a/lib/screens/banking/banking_screen.dart +++ b/lib/screens/banking/banking_screen.dart @@ -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'; @@ -59,14 +59,59 @@ class _BankingScreenState extends State // Get wallet provider for private key final walletProvider = Provider.of(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(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 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, ); @@ -74,21 +119,16 @@ class _BankingScreenState extends State // 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(); @@ -102,6 +142,48 @@ class _BankingScreenState extends State } } + // Show PIN dialog for private key access + Future _showPinDialog(BuildContext context) async { + final pinController = TextEditingController(); + + return showDialog( + 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( diff --git a/lib/services/wallet_service.dart b/lib/services/wallet_service.dart new file mode 100644 index 0000000..95850ae --- /dev/null +++ b/lib/services/wallet_service.dart @@ -0,0 +1,155 @@ +import 'dart:convert'; +import 'package:http/http.dart' as http; +import '../models/wallet.dart'; +import '../models/transaction_model.dart'; + +class WalletService { + static const String _baseUrl = 'http://localhost:8080'; // Default Fuego RPC endpoint + + // Singleton pattern + static final WalletService _instance = WalletService._internal(); + factory WalletService() => _instance; + WalletService._internal(); + + /// Get wallet balance + Future getBalance(String address) async { + try { + final response = await http.post( + Uri.parse('$_baseUrl/json_rpc'), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({ + 'jsonrpc': '2.0', + 'id': '0', + 'method': 'get_balance', + 'params': {'address': address} + }), + ); + + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + return data['result']['balance'].toString(); + } + throw Exception('Failed to get balance: ${response.statusCode}'); + } catch (e) { + throw Exception('Error getting balance: $e'); + } + } + + /// Get wallet address + Future getAddress() async { + try { + final response = await http.post( + Uri.parse('$_baseUrl/json_rpc'), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({ + 'jsonrpc': '2.0', + 'id': '0', + 'method': 'get_address' + }), + ); + + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + return data['result']['address']; + } + throw Exception('Failed to get address: ${response.statusCode}'); + } catch (e) { + throw Exception('Error getting address: $e'); + } + } + + /// Create a new wallet + Future createWallet() async { + try { + final response = await http.post( + Uri.parse('$_baseUrl/json_rpc'), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({ + 'jsonrpc': '2.0', + 'id': '0', + 'method': 'create_wallet' + }), + ); + + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + return Wallet.fromJson(data['result']); + } + throw Exception('Failed to create wallet: ${response.statusCode}'); + } catch (e) { + throw Exception('Error creating wallet: $e'); + } + } + + /// Send transaction + Future sendTransaction({ + required String toAddress, + required String amount, + required String privateKey, + }) async { + try { + final response = await http.post( + Uri.parse('$_baseUrl/json_rpc'), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({ + 'jsonrpc': '2.0', + 'id': '0', + 'method': 'send_transaction', + 'params': { + 'to_address': toAddress, + 'amount': amount, + 'private_key': privateKey, + } + }), + ); + + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + return data['result']['tx_hash']; + } + throw Exception('Failed to send transaction: ${response.statusCode}'); + } catch (e) { + throw Exception('Error sending transaction: $e'); + } + } + + /// Get transaction history + Future>> getTransactionHistory() async { + try { + final response = await http.post( + Uri.parse('$_baseUrl/json_rpc'), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({ + 'jsonrpc': '2.0', + 'id': '0', + 'method': 'get_transaction_history' + }), + ); + + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + return List>.from(data['result']['transactions']); + } + throw Exception('Failed to get transaction history: ${response.statusCode}'); + } catch (e) { + throw Exception('Error getting transaction history: $e'); + } + } + + /// Get transactions as TransactionModel objects + Future> getTransactions() async { + try { + final List> transactionData = await getTransactionHistory(); + return transactionData.map((data) => TransactionModel.fromJson(data)).toList(); + } catch (e) { + throw Exception('Error getting transactions: $e'); + } + } + + /// Check if transaction is a burn transaction + bool isBurnTransaction(Map transaction) { + return transaction['type'] == 'burn' || + transaction['to_address'] == null || + transaction['to_address'].isEmpty; + } +} \ No newline at end of file