diff --git a/lib/screens/wallet_screen.dart b/lib/screens/wallet_screen.dart index df5b680..fc37223 100644 --- a/lib/screens/wallet_screen.dart +++ b/lib/screens/wallet_screen.dart @@ -3,6 +3,7 @@ import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import '../providers/wallet_provider.dart'; +import '../utils/validation.dart'; import '../widgets/transaction_button.dart'; class WalletScreen extends StatefulWidget { @@ -24,9 +25,14 @@ class _WalletScreenState extends State { void _onConnect(WalletProvider provider) { final key = _secretKeyController.text.trim(); - if (key.isEmpty) return; + final error = Validation.stellarSecretKey(key); + if (error != null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(error), backgroundColor: Colors.red), + ); + return; + } provider.connect(key); - // Clear the field immediately so the key is not held in memory longer than needed _secretKeyController.clear(); } diff --git a/lib/utils/validation.dart b/lib/utils/validation.dart new file mode 100644 index 0000000..104f22e --- /dev/null +++ b/lib/utils/validation.dart @@ -0,0 +1,26 @@ +class Validation { + /// Returns null if valid, or an error message if invalid. + static String? stellarSecretKey(String key) { + final trimmed = key.trim(); + if (trimmed.isEmpty) return 'Secret key is required'; + if (!trimmed.startsWith('S')) return "Secret key must start with 'S'"; + if (trimmed.length != 56) { + return 'Secret key must be exactly 56 characters (got ${trimmed.length})'; + } + final base32 = RegExp(r'^[A-Z2-7]+$'); + if (!base32.hasMatch(trimmed)) { + return 'Secret key contains invalid characters (must be A-Z and 2-7)'; + } + return null; + } + + static String? stellarPublicKey(String key) { + final trimmed = key.trim(); + if (trimmed.isEmpty) return 'Address is required'; + if (!trimmed.startsWith('G')) return "Stellar address must start with 'G'"; + if (trimmed.length != 56) { + return 'Stellar address must be exactly 56 characters (got ${trimmed.length})'; + } + return null; + } +} diff --git a/test/utils/validation_test.dart b/test/utils/validation_test.dart new file mode 100644 index 0000000..bea58ba --- /dev/null +++ b/test/utils/validation_test.dart @@ -0,0 +1,51 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:nodus_protocol/utils/validation.dart'; + +void main() { + group('Validation.stellarSecretKey', () { + test('valid 56-char S-key returns null', () { + const key = 'SCZANGBA5AKIA7ORYH4RO2EQVN3ISYIKXT6EU7EOSQ37NQBP5M4OBK3A'; + expect(Validation.stellarSecretKey(key), isNull); + }); + + test('empty string fails', () { + expect(Validation.stellarSecretKey(''), isNotNull); + }); + + test('wrong prefix fails', () { + expect(Validation.stellarSecretKey('GAAAAAAABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB'), isNotNull); + }); + + test('wrong length fails', () { + expect(Validation.stellarSecretKey('SSHORT'), isNotNull); + }); + + test('invalid base32 chars fail', () { + expect(Validation.stellarSecretKey('S${'1' * 55}'), isNotNull); + }); + + test('valid key with spaces trims and validates', () { + const key = ' SCZANGBA5AKIA7ORYH4RO2EQVN3ISYIKXT6EU7EOSQ37NQBP5M4OBK3A '; + expect(Validation.stellarSecretKey(key), isNull); + }); + }); + + group('Validation.stellarPublicKey', () { + test('valid 56-char G-key returns null', () { + const key = 'GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAXJH'; + expect(Validation.stellarPublicKey(key), isNull); + }); + + test('empty string fails', () { + expect(Validation.stellarPublicKey(''), isNotNull); + }); + + test('wrong prefix fails', () { + expect(Validation.stellarPublicKey('SAAAAAAABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB'), isNotNull); + }); + + test('wrong length fails', () { + expect(Validation.stellarPublicKey('GSHORT'), isNotNull); + }); + }); +}