|
9 | 9 | */ |
10 | 10 |
|
11 | 11 | import 'dart:async'; |
| 12 | +import 'dart:convert'; |
12 | 13 | import 'dart:io'; |
13 | 14 | import 'dart:math'; |
14 | 15 | import 'package:flutter/material.dart'; |
15 | 16 | import 'package:logger/logger.dart'; |
16 | 17 | import 'package:cs_salvium/cs_salvium.dart' as lib_salvium; |
| 18 | +import 'package:tuple/tuple.dart'; |
17 | 19 | import '../../../utilities/logger.dart'; |
18 | 20 | import '../../../utilities/stack_file_system.dart'; |
| 21 | +import '../../../pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart'; |
19 | 22 | import '../test_suite_interface.dart'; |
20 | 23 | import '../testing_models.dart'; |
21 | 24 |
|
@@ -46,7 +49,9 @@ class SalviumIntegrationTestSuite implements TestSuiteInterface { |
46 | 49 | Logging.instance.log(Level.info, "Starting Salvium integration test suite..."); |
47 | 50 |
|
48 | 51 | await _testSalviumMnemonicGeneration(); |
49 | | - |
| 52 | + |
| 53 | + await _testSalviumStackWalletBackupRoundTrip(); |
| 54 | + |
50 | 55 | stopwatch.stop(); |
51 | 56 | _updateStatus(TestSuiteStatus.passed); |
52 | 57 |
|
@@ -188,6 +193,252 @@ class SalviumIntegrationTestSuite implements TestSuiteInterface { |
188 | 193 | } |
189 | 194 | } |
190 | 195 |
|
| 196 | + /// Tests Stack Wallet Backup round-trip functionality. |
| 197 | + /// |
| 198 | + /// Creates Salvium wallets with 25-word mnemonics, saves the mnemonics, |
| 199 | + /// creates backups, restores the backups, and verifies the restored mnemonics match the originals. |
| 200 | + Future<void> _testSalviumStackWalletBackupRoundTrip() async { |
| 201 | + Logging.instance.log(Level.info, "Testing Stack Wallet Backup round-trip for Salvium..."); |
| 202 | + |
| 203 | + final tempDir = await StackFileSystem.applicationRootDirectory(); |
| 204 | + final testId = Random().nextInt(10000); |
| 205 | + |
| 206 | + try { |
| 207 | + // Test 25-word mnemonic backup (Salvium only supports 25-word mnemonics). |
| 208 | + await _testSalviumBackupWithSeedType( |
| 209 | + tempDir: tempDir, |
| 210 | + testId: testId, |
| 211 | + seedType: lib_salvium.SalviumSeedType.twentyFive, |
| 212 | + expectedWordCount: 25, |
| 213 | + suffix: "25", |
| 214 | + ); |
| 215 | + |
| 216 | + Logging.instance.log(Level.info, "✓ Salvium Stack Wallet Backup round-trip test passed successfully!"); |
| 217 | + } catch (e) { |
| 218 | + Logging.instance.log(Level.error, "Salvium Stack Wallet Backup round-trip test failed: $e"); |
| 219 | + rethrow; |
| 220 | + } |
| 221 | + } |
| 222 | + |
| 223 | + /// Tests Stack Wallet Backup round-trip functionality for Salvium. |
| 224 | + Future<void> _testSalviumBackupWithSeedType({ |
| 225 | + required Directory tempDir, |
| 226 | + required int testId, |
| 227 | + required lib_salvium.SalviumSeedType seedType, |
| 228 | + required int expectedWordCount, |
| 229 | + required String suffix, |
| 230 | + }) async { |
| 231 | + Logging.instance.log(Level.info, "Testing ${expectedWordCount}-word Salvium mnemonic backup..."); |
| 232 | + |
| 233 | + final walletName = "test_salvium_backup_${testId}_$suffix"; |
| 234 | + final walletPath = "${tempDir.path}/$walletName"; |
| 235 | + final backupPath = "${tempDir.path}/${walletName}_backup.swb"; |
| 236 | + const walletPassword = "testpass123"; |
| 237 | + const backupPassword = "backuppass456"; |
| 238 | + |
| 239 | + lib_salvium.Wallet? originalWallet; |
| 240 | + String? originalMnemonic; |
| 241 | + |
| 242 | + try { |
| 243 | + // Step 1: Create a new Salvium wallet using lib_salvium directly. |
| 244 | + Logging.instance.log(Level.info, "Step 1: Creating new ${expectedWordCount}-word Salvium wallet..."); |
| 245 | + |
| 246 | + originalWallet = await lib_salvium.SalviumWallet.create( |
| 247 | + path: walletPath, |
| 248 | + password: walletPassword, |
| 249 | + seedType: seedType, |
| 250 | + seedOffset: "", |
| 251 | + ); |
| 252 | + |
| 253 | + // Step 2: Save the original mnemonic out-of-band. |
| 254 | + Logging.instance.log(Level.info, "Step 2: Saving original mnemonic..."); |
| 255 | + originalMnemonic = originalWallet.getSeed(); |
| 256 | + |
| 257 | + if (originalMnemonic.isEmpty) { |
| 258 | + throw Exception("Failed to retrieve mnemonic from created Salvium wallet"); |
| 259 | + } |
| 260 | + |
| 261 | + final originalWords = originalMnemonic.split(' '); |
| 262 | + Logging.instance.log(Level.info, "Original Salvium mnemonic has ${originalWords.length} words"); |
| 263 | + |
| 264 | + // Validate the mnemonic format. |
| 265 | + if (originalWords.length != expectedWordCount) { |
| 266 | + throw Exception("Expected ${expectedWordCount}-word mnemonic, got ${originalWords.length} words"); |
| 267 | + } |
| 268 | + |
| 269 | + // Step 3: Create a Stack Wallet Backup. |
| 270 | + Logging.instance.log(Level.info, "Step 3: Creating Stack Wallet Backup..."); |
| 271 | + |
| 272 | + // Create a minimal backup JSON with just our test wallet. |
| 273 | + final backupJson = { |
| 274 | + "wallets": [ |
| 275 | + { |
| 276 | + "name": walletName, |
| 277 | + "id": "test_salvium_wallet_${testId}_$suffix", |
| 278 | + "mnemonic": originalMnemonic, |
| 279 | + "mnemonicPassphrase": "", |
| 280 | + "coinName": "salvium", |
| 281 | + "storedChainHeight": 0, |
| 282 | + "restoreHeight": 0, |
| 283 | + "notes": {}, |
| 284 | + "isFavorite": false, |
| 285 | + "otherDataJsonString": null, |
| 286 | + } |
| 287 | + ], |
| 288 | + "prefs": { |
| 289 | + "currency": "USD", |
| 290 | + "useBiometrics": false, |
| 291 | + "hasPin": false, |
| 292 | + "language": "en", |
| 293 | + "showFavoriteWallets": true, |
| 294 | + "wifiOnly": false, |
| 295 | + "syncType": "allWalletsOnStartup", |
| 296 | + "walletIdsSyncOnStartup": [], |
| 297 | + "showTestNetCoins": false, |
| 298 | + "isAutoBackupEnabled": false, |
| 299 | + "autoBackupLocation": null, |
| 300 | + "backupFrequencyType": "BackupFrequencyType.everyAppStart", |
| 301 | + "lastAutoBackup": DateTime.now().toString(), |
| 302 | + }, |
| 303 | + "nodes": [], |
| 304 | + "addressBookEntries": [], |
| 305 | + "tradeHistory": [], |
| 306 | + "tradeTxidLookupData": [], |
| 307 | + "tradeNotes": {}, |
| 308 | + }; |
| 309 | + |
| 310 | + final jsonString = jsonEncode(backupJson); |
| 311 | + |
| 312 | + // Encrypt and save the backup. |
| 313 | + final success = await SWB.encryptStackWalletWithPassphrase( |
| 314 | + backupPath, |
| 315 | + backupPassword, |
| 316 | + jsonString, |
| 317 | + ); |
| 318 | + |
| 319 | + if (!success) { |
| 320 | + throw Exception("Failed to create Stack Wallet Backup for Salvium"); |
| 321 | + } |
| 322 | + |
| 323 | + Logging.instance.log(Level.info, "Backup created successfully at: $backupPath"); |
| 324 | + |
| 325 | + // Step 4: Restore the Stack Wallet Backup. |
| 326 | + Logging.instance.log(Level.info, "Step 4: Restoring Stack Wallet Backup..."); |
| 327 | + |
| 328 | + final restoredJsonString = await SWB.decryptStackWalletWithPassphrase( |
| 329 | + Tuple2(backupPath, backupPassword), |
| 330 | + ); |
| 331 | + |
| 332 | + if (restoredJsonString == null) { |
| 333 | + throw Exception("Failed to decrypt Stack Wallet Backup for Salvium"); |
| 334 | + } |
| 335 | + |
| 336 | + final restoredJson = jsonDecode(restoredJsonString) as Map<String, dynamic>; |
| 337 | + final restoredWallets = restoredJson["wallets"] as List<dynamic>; |
| 338 | + |
| 339 | + if (restoredWallets.isEmpty) { |
| 340 | + throw Exception("No wallets found in restored Salvium backup"); |
| 341 | + } |
| 342 | + |
| 343 | + final restoredWalletData = restoredWallets.first as Map<String, dynamic>; |
| 344 | + final restoredMnemonic = restoredWalletData["mnemonic"] as String; |
| 345 | + |
| 346 | + // Step 5: Verify that the restored mnemonic matches the original. |
| 347 | + Logging.instance.log(Level.info, "Step 5: Verifying Salvium mnemonic integrity..."); |
| 348 | + |
| 349 | + if (restoredMnemonic != originalMnemonic) { |
| 350 | + throw Exception( |
| 351 | + "Salvium mnemonic mismatch!\n" |
| 352 | + "Original: $originalMnemonic\n" |
| 353 | + "Restored: $restoredMnemonic" |
| 354 | + ); |
| 355 | + } |
| 356 | + |
| 357 | + // Additional verification: check word count. |
| 358 | + final restoredWords = restoredMnemonic.split(' '); |
| 359 | + |
| 360 | + if (originalWords.length != restoredWords.length) { |
| 361 | + throw Exception( |
| 362 | + "Word count mismatch: original ${originalWords.length}, restored ${restoredWords.length}" |
| 363 | + ); |
| 364 | + } |
| 365 | + |
| 366 | + // Verify each word matches. |
| 367 | + for (int i = 0; i < originalWords.length; i++) { |
| 368 | + if (originalWords[i] != restoredWords[i]) { |
| 369 | + throw Exception( |
| 370 | + "Word mismatch at position $i: '${originalWords[i]}' != '${restoredWords[i]}'" |
| 371 | + ); |
| 372 | + } |
| 373 | + } |
| 374 | + |
| 375 | + // Step 6: Additional test - verify we can recreate the wallet from the restored mnemonic. |
| 376 | + Logging.instance.log(Level.info, "Step 6: Testing Salvium wallet restoration with recovered mnemonic..."); |
| 377 | + |
| 378 | + final testWalletPath = "${tempDir.path}/test_salvium_restore_${testId}_$suffix"; |
| 379 | + lib_salvium.Wallet? restoredWallet; |
| 380 | + |
| 381 | + try { |
| 382 | + restoredWallet = await lib_salvium.SalviumWallet.restoreWalletFromSeed( |
| 383 | + path: testWalletPath, |
| 384 | + password: walletPassword, |
| 385 | + seed: restoredMnemonic, |
| 386 | + restoreHeight: 0, |
| 387 | + seedOffset: "", |
| 388 | + ); |
| 389 | + |
| 390 | + final restoredMnemonicFromWallet = restoredWallet.getSeed(); |
| 391 | + |
| 392 | + if (restoredMnemonicFromWallet != originalMnemonic) { |
| 393 | + throw Exception( |
| 394 | + "Restored Salvium wallet mnemonic doesn't match original!\n" |
| 395 | + "Original: $originalMnemonic\n" |
| 396 | + "From restored wallet: $restoredMnemonicFromWallet" |
| 397 | + ); |
| 398 | + } |
| 399 | + |
| 400 | + Logging.instance.log(Level.info, "✓ Successfully restored ${expectedWordCount}-word Salvium wallet from backup mnemonic"); |
| 401 | + |
| 402 | + } finally { |
| 403 | + await restoredWallet?.close(); |
| 404 | + // Clean up restored wallet files. |
| 405 | + final testWalletFile = File(testWalletPath); |
| 406 | + final testKeysFile = File("$testWalletPath.keys"); |
| 407 | + final testAddressFile = File("$testWalletPath.address.txt"); |
| 408 | + |
| 409 | + if (await testWalletFile.exists()) await testWalletFile.delete(); |
| 410 | + if (await testKeysFile.exists()) await testKeysFile.delete(); |
| 411 | + if (await testAddressFile.exists()) await testAddressFile.delete(); |
| 412 | + } |
| 413 | + |
| 414 | + Logging.instance.log(Level.info, "✓ ${expectedWordCount}-word Salvium Stack Wallet Backup round-trip test passed!"); |
| 415 | + Logging.instance.log(Level.info, "✓ Original and restored Salvium mnemonics match perfectly"); |
| 416 | + Logging.instance.log(Level.info, "✓ Verified ${originalWords.length}-word Salvium mnemonic integrity"); |
| 417 | + Logging.instance.log(Level.info, "✓ Confirmed ${expectedWordCount}-word Salvium wallet can be restored from backup mnemonic"); |
| 418 | + |
| 419 | + } finally { |
| 420 | + // Cleanup. |
| 421 | + try { |
| 422 | + await originalWallet?.close(); |
| 423 | + |
| 424 | + // Clean up test files. |
| 425 | + final walletFile = File(walletPath); |
| 426 | + final keysFile = File("$walletPath.keys"); |
| 427 | + final addressFile = File("$walletPath.address.txt"); |
| 428 | + final backupFile = File(backupPath); |
| 429 | + |
| 430 | + if (await walletFile.exists()) await walletFile.delete(); |
| 431 | + if (await keysFile.exists()) await keysFile.delete(); |
| 432 | + if (await addressFile.exists()) await addressFile.delete(); |
| 433 | + if (await backupFile.exists()) await backupFile.delete(); |
| 434 | + |
| 435 | + Logging.instance.log(Level.info, "Cleaned up test files for ${expectedWordCount}-word Salvium backup test"); |
| 436 | + } catch (e) { |
| 437 | + Logging.instance.log(Level.warning, "Cleanup error for ${expectedWordCount}-word Salvium test: $e"); |
| 438 | + } |
| 439 | + } |
| 440 | + } |
| 441 | + |
191 | 442 | void _updateStatus(TestSuiteStatus newStatus) { |
192 | 443 | _status = newStatus; |
193 | 444 | _statusController.add(newStatus); |
|
0 commit comments