Skip to content

Commit 0d2ee6f

Browse files
committed
test(monero): add Monero SWB round trip test
1 parent 83e7c82 commit 0d2ee6f

File tree

2 files changed

+271
-12
lines changed

2 files changed

+271
-12
lines changed

lib/services/testing/test_suites/monero_integration_test_suite.dart

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,17 @@
99
*/
1010

1111
import 'dart:async';
12+
import 'dart:convert';
1213
import 'dart:io';
1314
import 'dart:math';
1415
import 'package:compat/old_cw_core/path_for_wallet.dart' as lib_monero_compat;
1516
import 'package:flutter/material.dart';
1617
import 'package:logger/logger.dart';
1718
import 'package:cs_monero/cs_monero.dart' as lib_monero;
19+
import 'package:tuple/tuple.dart';
1820
import '../../../utilities/logger.dart';
1921
import '../../../utilities/stack_file_system.dart';
22+
import '../../../pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart';
2023
import '../test_suite_interface.dart';
2124
import '../testing_models.dart';
2225
import 'test_data/polyseed_vectors.dart';
@@ -49,6 +52,8 @@ class MoneroWalletTestSuite implements TestSuiteInterface {
4952

5053
await _testMnemonicGeneration();
5154

55+
await _testStackWalletBackupRoundTrip();
56+
5257
// TODO: FIXME.
5358
// await _testPolyseedRestoration();
5459

@@ -278,6 +283,261 @@ class MoneroWalletTestSuite implements TestSuiteInterface {
278283
}
279284
}
280285

286+
/// Tests Stack Wallet Backup round-trip functionality.
287+
///
288+
/// Creates Monero wallets with both 16-word and 25-word mnemonics, saves the mnemonics,
289+
/// creates backups, restores the backups, and verifies the restored mnemonics match the originals.
290+
Future<void> _testStackWalletBackupRoundTrip() async {
291+
Logging.instance.log(Level.info, "Testing Stack Wallet Backup round-trip for Monero...");
292+
293+
final tempDir = await StackFileSystem.applicationRootDirectory();
294+
final testId = Random().nextInt(10000);
295+
296+
try {
297+
// Test 16-word mnemonic backup.
298+
await _testBackupWithSeedType(
299+
tempDir: tempDir,
300+
testId: testId,
301+
seedType: lib_monero.MoneroSeedType.sixteen,
302+
expectedWordCount: 16,
303+
suffix: "16",
304+
);
305+
306+
// Test 25-word mnemonic backup.
307+
await _testBackupWithSeedType(
308+
tempDir: tempDir,
309+
testId: testId,
310+
seedType: lib_monero.MoneroSeedType.twentyFive,
311+
expectedWordCount: 25,
312+
suffix: "25",
313+
);
314+
315+
Logging.instance.log(Level.info, "✓ All Stack Wallet Backup round-trip tests passed successfully!");
316+
} catch (e) {
317+
Logging.instance.log(Level.error, "Stack Wallet Backup round-trip test failed: $e");
318+
rethrow;
319+
}
320+
}
321+
322+
/// Tests Stack Wallet Backup round-trip functionality for a specific seed type.
323+
Future<void> _testBackupWithSeedType({
324+
required Directory tempDir,
325+
required int testId,
326+
required lib_monero.MoneroSeedType seedType,
327+
required int expectedWordCount,
328+
required String suffix,
329+
}) async {
330+
Logging.instance.log(Level.info, "Testing ${expectedWordCount}-word mnemonic backup...");
331+
332+
final walletName = "test_monero_backup_${testId}_$suffix";
333+
final walletPath = "${tempDir.path}/$walletName";
334+
final backupPath = "${tempDir.path}/${walletName}_backup.swb";
335+
const walletPassword = "testpass123";
336+
const backupPassword = "backuppass456";
337+
338+
lib_monero.Wallet? originalWallet;
339+
String? originalMnemonic;
340+
341+
try {
342+
// Step 1: Create a new Monero wallet using lib_monero directly.
343+
Logging.instance.log(Level.info, "Step 1: Creating new ${expectedWordCount}-word Monero wallet...");
344+
345+
originalWallet = await lib_monero.MoneroWallet.create(
346+
path: walletPath,
347+
password: walletPassword,
348+
seedType: seedType,
349+
seedOffset: "",
350+
);
351+
352+
// Step 2: Save the original mnemonic out-of-band.
353+
Logging.instance.log(Level.info, "Step 2: Saving original mnemonic...");
354+
originalMnemonic = await originalWallet.getSeed();
355+
356+
if (originalMnemonic.isEmpty) {
357+
throw Exception("Failed to retrieve mnemonic from created wallet");
358+
}
359+
360+
final originalWords = originalMnemonic.split(' ');
361+
Logging.instance.log(Level.info, "Original mnemonic has ${originalWords.length} words");
362+
363+
// Validate the mnemonic format.
364+
if (originalWords.length != expectedWordCount) {
365+
throw Exception("Expected ${expectedWordCount}-word mnemonic, got ${originalWords.length} words");
366+
}
367+
368+
// Step 3: Create a Stack Wallet Backup.
369+
Logging.instance.log(Level.info, "Step 3: Creating Stack Wallet Backup...");
370+
371+
// Create a minimal backup JSON with just our test wallet.
372+
final backupJson = {
373+
"wallets": [
374+
{
375+
"name": walletName,
376+
"id": "test_wallet_${testId}_$suffix",
377+
"mnemonic": originalMnemonic,
378+
"mnemonicPassphrase": "",
379+
"coinName": "monero",
380+
"storedChainHeight": 0,
381+
"restoreHeight": 0,
382+
"notes": {},
383+
"isFavorite": false,
384+
"otherDataJsonString": null,
385+
}
386+
],
387+
"prefs": {
388+
"currency": "USD",
389+
"useBiometrics": false,
390+
"hasPin": false,
391+
"language": "en",
392+
"showFavoriteWallets": true,
393+
"wifiOnly": false,
394+
"syncType": "allWalletsOnStartup",
395+
"walletIdsSyncOnStartup": [],
396+
"showTestNetCoins": false,
397+
"isAutoBackupEnabled": false,
398+
"autoBackupLocation": null,
399+
"backupFrequencyType": "BackupFrequencyType.everyAppStart",
400+
"lastAutoBackup": DateTime.now().toString(),
401+
},
402+
"nodes": [],
403+
"addressBookEntries": [],
404+
"tradeHistory": [],
405+
"tradeTxidLookupData": [],
406+
"tradeNotes": {},
407+
};
408+
409+
final jsonString = jsonEncode(backupJson);
410+
411+
// Encrypt and save the backup.
412+
final success = await SWB.encryptStackWalletWithPassphrase(
413+
backupPath,
414+
backupPassword,
415+
jsonString,
416+
);
417+
418+
if (!success) {
419+
throw Exception("Failed to create Stack Wallet Backup");
420+
}
421+
422+
Logging.instance.log(Level.info, "Backup created successfully at: $backupPath");
423+
424+
// Step 4: Restore the Stack Wallet Backup.
425+
Logging.instance.log(Level.info, "Step 4: Restoring Stack Wallet Backup...");
426+
427+
final restoredJsonString = await SWB.decryptStackWalletWithPassphrase(
428+
Tuple2(backupPath, backupPassword),
429+
);
430+
431+
if (restoredJsonString == null) {
432+
throw Exception("Failed to decrypt Stack Wallet Backup");
433+
}
434+
435+
final restoredJson = jsonDecode(restoredJsonString) as Map<String, dynamic>;
436+
final restoredWallets = restoredJson["wallets"] as List<dynamic>;
437+
438+
if (restoredWallets.isEmpty) {
439+
throw Exception("No wallets found in restored backup");
440+
}
441+
442+
final restoredWalletData = restoredWallets.first as Map<String, dynamic>;
443+
final restoredMnemonic = restoredWalletData["mnemonic"] as String;
444+
445+
// Step 5: Verify that the restored mnemonic matches the original.
446+
Logging.instance.log(Level.info, "Step 5: Verifying mnemonic integrity...");
447+
448+
if (restoredMnemonic != originalMnemonic) {
449+
throw Exception(
450+
"Mnemonic mismatch!\n"
451+
"Original: $originalMnemonic\n"
452+
"Restored: $restoredMnemonic"
453+
);
454+
}
455+
456+
// Additional verification: check word count.
457+
final restoredWords = restoredMnemonic.split(' ');
458+
459+
if (originalWords.length != restoredWords.length) {
460+
throw Exception(
461+
"Word count mismatch: original ${originalWords.length}, restored ${restoredWords.length}"
462+
);
463+
}
464+
465+
// Verify each word matches.
466+
for (int i = 0; i < originalWords.length; i++) {
467+
if (originalWords[i] != restoredWords[i]) {
468+
throw Exception(
469+
"Word mismatch at position $i: '${originalWords[i]}' != '${restoredWords[i]}'"
470+
);
471+
}
472+
}
473+
474+
// Step 6: Additional test - verify we can recreate the wallet from the restored mnemonic.
475+
Logging.instance.log(Level.info, "Step 6: Testing wallet restoration with recovered mnemonic...");
476+
477+
final testWalletPath = "${tempDir.path}/test_restore_${testId}_$suffix";
478+
lib_monero.Wallet? restoredWallet;
479+
480+
try {
481+
restoredWallet = await lib_monero.MoneroWallet.restoreWalletFromSeed(
482+
path: testWalletPath,
483+
password: walletPassword,
484+
seed: restoredMnemonic,
485+
restoreHeight: 0,
486+
seedOffset: "",
487+
);
488+
489+
final restoredMnemonicFromWallet = await restoredWallet.getSeed();
490+
491+
if (restoredMnemonicFromWallet != originalMnemonic) {
492+
throw Exception(
493+
"Restored wallet mnemonic doesn't match original!\n"
494+
"Original: $originalMnemonic\n"
495+
"From restored wallet: $restoredMnemonicFromWallet"
496+
);
497+
}
498+
499+
Logging.instance.log(Level.info, "✓ Successfully restored ${expectedWordCount}-word wallet from backup mnemonic");
500+
501+
} finally {
502+
await restoredWallet?.close();
503+
// Clean up restored wallet files.
504+
final testWalletFile = File(testWalletPath);
505+
final testKeysFile = File("$testWalletPath.keys");
506+
final testAddressFile = File("$testWalletPath.address.txt");
507+
508+
if (await testWalletFile.exists()) await testWalletFile.delete();
509+
if (await testKeysFile.exists()) await testKeysFile.delete();
510+
if (await testAddressFile.exists()) await testAddressFile.delete();
511+
}
512+
513+
Logging.instance.log(Level.info, "✓ ${expectedWordCount}-word Stack Wallet Backup round-trip test passed!");
514+
Logging.instance.log(Level.info, "✓ Original and restored mnemonics match perfectly");
515+
Logging.instance.log(Level.info, "✓ Verified ${originalWords.length}-word mnemonic integrity");
516+
Logging.instance.log(Level.info, "✓ Confirmed ${expectedWordCount}-word wallet can be restored from backup mnemonic");
517+
518+
} finally {
519+
// Cleanup.
520+
try {
521+
await originalWallet?.close();
522+
523+
// Clean up test files.
524+
final walletFile = File(walletPath);
525+
final keysFile = File("$walletPath.keys");
526+
final addressFile = File("$walletPath.address.txt");
527+
final backupFile = File(backupPath);
528+
529+
if (await walletFile.exists()) await walletFile.delete();
530+
if (await keysFile.exists()) await keysFile.delete();
531+
if (await addressFile.exists()) await addressFile.delete();
532+
if (await backupFile.exists()) await backupFile.delete();
533+
534+
Logging.instance.log(Level.info, "Cleaned up test files for ${expectedWordCount}-word Monero backup test");
535+
} catch (e) {
536+
Logging.instance.log(Level.warning, "Cleanup error for ${expectedWordCount}-word test: $e");
537+
}
538+
}
539+
}
540+
281541
void _updateStatus(TestSuiteStatus newStatus) {
282542
_status = newStatus;
283543
_statusController.add(newStatus);

pubspec.lock

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -813,11 +813,11 @@ packages:
813813
dependency: "direct main"
814814
description:
815815
path: "."
816-
ref: f0b1300140d45c13e7722f8f8d20308efeba8449
817-
resolved-ref: f0b1300140d45c13e7722f8f8d20308efeba8449
816+
ref: "794ab2d7b88b34d64a89518f9b9f41dcc235aca1"
817+
resolved-ref: "794ab2d7b88b34d64a89518f9b9f41dcc235aca1"
818818
url: "https://github.com/cypherstack/electrum_adapter.git"
819819
source: git
820-
version: "3.0.0"
820+
version: "3.0.2"
821821
emojis:
822822
dependency: "direct main"
823823
description:
@@ -1119,8 +1119,8 @@ packages:
11191119
dependency: "direct main"
11201120
description:
11211121
path: "."
1122-
ref: afaad488f5215a9c2c211e5e2f8460237eef60f1
1123-
resolved-ref: afaad488f5215a9c2c211e5e2f8460237eef60f1
1122+
ref: "540d0bc7dc27a97d45d63f412f26818a7f3b8b51"
1123+
resolved-ref: "540d0bc7dc27a97d45d63f412f26818a7f3b8b51"
11241124
url: "https://github.com/cypherstack/fusiondart.git"
11251125
source: git
11261126
version: "1.0.0"
@@ -1963,11 +1963,10 @@ packages:
19631963
socks_socket:
19641964
dependency: transitive
19651965
description:
1966-
path: "."
1967-
ref: master
1968-
resolved-ref: e6232c53c1595469931ababa878759a067c02e94
1969-
url: "https://github.com/cypherstack/socks_socket.git"
1970-
source: git
1966+
name: socks_socket
1967+
sha256: "53bc7eae40a3aa16ea810b0e9de3bb23ba7beb0b40d09357b89190f2f44374cc"
1968+
url: "https://pub.dev"
1969+
source: hosted
19711970
version: "1.1.1"
19721971
solana:
19731972
dependency: "direct main"
@@ -2200,8 +2199,8 @@ packages:
22002199
dependency: "direct main"
22012200
description:
22022201
path: "."
2203-
ref: "752f054b65c500adb9cad578bf183a978e012502"
2204-
resolved-ref: "752f054b65c500adb9cad578bf183a978e012502"
2202+
ref: "16c9e709e984ec89e8715ce378b038c93ad7add3"
2203+
resolved-ref: "16c9e709e984ec89e8715ce378b038c93ad7add3"
22052204
url: "https://github.com/cypherstack/tor.git"
22062205
source: git
22072206
version: "0.0.1"

0 commit comments

Comments
 (0)