From 761644686b69d396c1dc2cec6ce0d87d78984ace Mon Sep 17 00:00:00 2001 From: subsubl Date: Mon, 1 Dec 2025 01:29:04 +0100 Subject: [PATCH 1/5] feat(wallet): add ExportViewingWallet helper and unit test --- .../UnitTests/ExportViewingWalletTests.cs | 51 +++++++++++++++++++ Spoke/Spoke/Meta/Node.cs | 43 ++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 Spoke.UnitTestsClean/UnitTests/ExportViewingWalletTests.cs diff --git a/Spoke.UnitTestsClean/UnitTests/ExportViewingWalletTests.cs b/Spoke.UnitTestsClean/UnitTests/ExportViewingWalletTests.cs new file mode 100644 index 0000000..2a1f777 --- /dev/null +++ b/Spoke.UnitTestsClean/UnitTests/ExportViewingWalletTests.cs @@ -0,0 +1,51 @@ +using System; +using System.IO; +using IXICore; +using IXICore.Meta; +using Spoke.Meta; +using Xunit; + +namespace Spoke.UnitTestsClean.UnitTests +{ + public class ExportViewingWalletTests : IDisposable + { + private readonly string _tempDir; + + public ExportViewingWalletTests() + { + _tempDir = Path.Combine(Path.GetTempPath(), "SpokeExportTest", Guid.NewGuid().ToString()); + Directory.CreateDirectory(_tempDir); + // Ensure Spoke uses our temp folder for wallet storage + Config.spokeUserFolder = _tempDir; + } + + public void Dispose() + { + try + { + if (Directory.Exists(_tempDir)) Directory.Delete(_tempDir, true); + } + catch { } + } + + [Fact] + public void ExportViewingWallet_CreatesViewFile() + { + string walletPath = Path.Combine(_tempDir, Node.walletFile); + + // Create a real WalletStorage and add to IxianHandler + WalletStorage ws = new WalletStorage(walletPath); + bool gen = ws.generateWallet("test-password-123"); + Assert.True(gen, "Failed to generate wallet for test"); + + bool added = IXICore.Meta.IxianHandler.addWallet(ws); + Assert.True(added, "Failed to add wallet to IxianHandler"); + + string? exported = Node.ExportViewingWallet(); + Assert.False(string.IsNullOrEmpty(exported)); + Assert.True(File.Exists(exported)); + var bytes = File.ReadAllBytes(exported); + Assert.True(bytes.Length > 0, "Exported view wallet should contain bytes"); + } + } +} diff --git a/Spoke/Spoke/Meta/Node.cs b/Spoke/Spoke/Meta/Node.cs index c34c068..1761b67 100644 --- a/Spoke/Spoke/Meta/Node.cs +++ b/Spoke/Spoke/Meta/Node.cs @@ -3,6 +3,7 @@ using IXICore.Network; using IXICore.RegNames; using Spoke.Network; +using System.IO; namespace Spoke.Meta; @@ -91,6 +92,48 @@ public void init() Logging.info("Spoke Node initialization complete"); } + /// + /// Export a view-only (viewing) wallet file for the currently loaded wallet + /// If destPath is null, writes to `Config.spokeUserFolder` as `wallet.view.ixi` and returns the written path. + /// Returns null on failure. + /// + public static string? ExportViewingWallet(string? destPath = null) + { + try + { + var ws = IxianHandler.getWalletStorage(); + if (ws == null) + { + Logging.error("No wallet storage available to export viewing wallet."); + return null; + } + + byte[] viewingBytes = ws.getRawViewingWallet(); + if (viewingBytes == null) + { + Logging.error("Failed to get viewing wallet bytes."); + return null; + } + + string outPath = destPath ?? Path.Combine(Config.spokeUserFolder, Path.GetFileNameWithoutExtension(walletFile) + ".view.ixi"); + string? directory = Path.GetDirectoryName(outPath); + if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + + File.WriteAllBytes(outPath, viewingBytes); + + Logging.info("Exported viewing wallet to {0}", outPath); + return outPath; + } + catch (Exception ex) + { + Logging.error("Exception exporting viewing wallet: {0}", ex.ToString()); + return null; + } + } + /// /// Checks for existing wallet file /// From b0809467e1d01bbd39a8b630aa44e47c1e424872 Mon Sep 17 00:00:00 2001 From: subsubl Date: Mon, 1 Dec 2025 01:29:35 +0100 Subject: [PATCH 2/5] chore(tests): pin Microsoft.NET.Test.Sdk to 18.0.1 for restore --- Spoke.UnitTestsClean/Spoke.UnitTestsClean.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Spoke.UnitTestsClean/Spoke.UnitTestsClean.csproj b/Spoke.UnitTestsClean/Spoke.UnitTestsClean.csproj index 1d66adf..4cb3bdd 100644 --- a/Spoke.UnitTestsClean/Spoke.UnitTestsClean.csproj +++ b/Spoke.UnitTestsClean/Spoke.UnitTestsClean.csproj @@ -7,7 +7,7 @@ - + From d6ac52daf0fb298070e1b67433a48cc6cafa39a4 Mon Sep 17 00:00:00 2001 From: subsubl Date: Mon, 1 Dec 2025 01:29:53 +0100 Subject: [PATCH 3/5] test: remove stale compile includes for legacy WalletManager/BridgeConnection --- Spoke.UnitTestsClean/Spoke.UnitTestsClean.csproj | 2 -- 1 file changed, 2 deletions(-) diff --git a/Spoke.UnitTestsClean/Spoke.UnitTestsClean.csproj b/Spoke.UnitTestsClean/Spoke.UnitTestsClean.csproj index 4cb3bdd..b5c42b0 100644 --- a/Spoke.UnitTestsClean/Spoke.UnitTestsClean.csproj +++ b/Spoke.UnitTestsClean/Spoke.UnitTestsClean.csproj @@ -10,7 +10,5 @@ - - \ No newline at end of file From 1493b8bd2b257f9ff669949217e69d3709adff60 Mon Sep 17 00:00:00 2001 From: subsubl Date: Mon, 1 Dec 2025 01:33:34 +0100 Subject: [PATCH 4/5] feat(wallet): add WalletAdapter.ExportViewingWallet wrapper; document viewing-wallet export in quickstart --- Spoke/Spoke/Wallet/WalletAdapter.cs | 16 ++++++++++++++++ specs/main/quickstart.md | 21 +++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/Spoke/Spoke/Wallet/WalletAdapter.cs b/Spoke/Spoke/Wallet/WalletAdapter.cs index 040e721..db8900b 100644 --- a/Spoke/Spoke/Wallet/WalletAdapter.cs +++ b/Spoke/Spoke/Wallet/WalletAdapter.cs @@ -42,5 +42,21 @@ public static class WalletAdapter } return null; } + + /// + /// Export a view-only wallet using the Node helper. + /// Returns the exported file path on success, null on failure. + /// + public static string? ExportViewingWallet(string? destPath = null) + { + try + { + return Spoke.Meta.Node.ExportViewingWallet(destPath); + } + catch + { + return null; + } + } } } diff --git a/specs/main/quickstart.md b/specs/main/quickstart.md index 81b21df..63536d1 100644 --- a/specs/main/quickstart.md +++ b/specs/main/quickstart.md @@ -34,3 +34,24 @@ Notes - CI: the repository workflow `/.github/workflows/ci-dotnet-maui.yml` runs workload restore, restore, build and tests on push and PR to `main`. If you need help installing platform-specific toolchains, see `specs/main/research.md` for recommendations and verification commands. + +Exporting a View-Only Wallet + +To create a view-only (viewing) wallet file that can be shared safely (does not contain private key material), Spoke provides an export helper that writes a view-only wallet file to disk. + +Default location: +- `~\Spoke\wallet.view.ixi` (on Windows this is under `%USERPROFILE%\Spoke` — controlled by `Config.spokeUserFolder`). + +Commands (developer) + +```powershell +# Export to default location (programmatic call via Node.ExportViewingWallet()) +# In the running app, invoke the export via the settings UI or programmatically: +# Spoke.Wallet.WalletAdapter.ExportViewingWallet() + +# If you need to export to a specific path, call the helper with a destination path. +``` + +Notes +- The exported viewing wallet is intended for read-only use and should not include private keys. Treat exported files as sensitive in transport and storage. +- User-facing export is available in the Settings → Backup screen when the export action is triggered. From 568f98f588992a39c53e341d4f00aa5b29dde0e9 Mon Sep 17 00:00:00 2001 From: subsubl Date: Mon, 1 Dec 2025 01:42:04 +0100 Subject: [PATCH 5/5] feat(ui): add Export View-Only Wallet button and share helper --- Spoke/Spoke/Pages/SettingsPage.xaml | 3 ++ Spoke/Spoke/Pages/SettingsPage.xaml.cs | 29 ++++++++++++++++++ Spoke/Spoke/Platforms/SFileOperations.cs | 39 ++++++++++++++++++++++++ 3 files changed, 71 insertions(+) create mode 100644 Spoke/Spoke/Platforms/SFileOperations.cs diff --git a/Spoke/Spoke/Pages/SettingsPage.xaml b/Spoke/Spoke/Pages/SettingsPage.xaml index 2027bd4..c05121b 100644 --- a/Spoke/Spoke/Pages/SettingsPage.xaml +++ b/Spoke/Spoke/Pages/SettingsPage.xaml @@ -67,6 +67,9 @@